diff --git a/frontend/app/web-gold/src/components/PromptSelector.vue b/frontend/app/web-gold/src/components/PromptSelector.vue index 7c444cc18f..d87070cbb9 100644 --- a/frontend/app/web-gold/src/components/PromptSelector.vue +++ b/frontend/app/web-gold/src/components/PromptSelector.vue @@ -15,7 +15,13 @@ :value="prompt.id" >
- {{ prompt.name }} +
+ {{ prompt.name }} + {{ prompt.category }} +
+ + {{ prompt.source === 'created' ? '自建' : '收藏' }} +
@@ -313,6 +319,37 @@ onMounted(() => { font-weight: 500; } +.prompt-option-left { + display: flex; + align-items: center; + gap: 8px; + flex: 1; + min-width: 0; +} + +.prompt-option-tag { + font-size: 12px; + padding: 2px 8px; + border-radius: 10px; + white-space: nowrap; + flex-shrink: 0; +} + +.prompt-option-tag.category { + background: rgba(24, 144, 255, 0.1); + color: var(--color-primary); +} + +.prompt-option-tag.created { + background: rgba(16, 185, 129, 0.1); + color: #10b981; +} + +.prompt-option-tag.favorite { + background: rgba(245, 158, 11, 0.1); + color: #f59e0b; +} + /* 标签模式 */ .prompt-tags-container { margin-bottom: 12px; diff --git a/frontend/app/web-gold/src/views/content-style/Benchmark.vue b/frontend/app/web-gold/src/views/content-style/Benchmark.vue index ce567ef921..92e7d3b87b 100644 --- a/frontend/app/web-gold/src/views/content-style/Benchmark.vue +++ b/frontend/app/web-gold/src/views/content-style/Benchmark.vue @@ -1,24 +1,16 @@ + + diff --git a/frontend/app/web-gold/src/views/system/task-management/layout/TaskLayout.vue b/frontend/app/web-gold/src/views/system/task-management/layout/TaskLayout.vue index 686c1525ee..3abe4eb6f4 100644 --- a/frontend/app/web-gold/src/views/system/task-management/layout/TaskLayout.vue +++ b/frontend/app/web-gold/src/views/system/task-management/layout/TaskLayout.vue @@ -27,7 +27,7 @@ - diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/benchmark/job/BenchmarkTaskExecutor.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/benchmark/job/BenchmarkTaskExecutor.java index e5d2a06c98..3660037ab1 100644 --- a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/benchmark/job/BenchmarkTaskExecutor.java +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/benchmark/job/BenchmarkTaskExecutor.java @@ -2,13 +2,20 @@ package cn.iocoder.yudao.module.tik.benchmark.job; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.module.tik.benchmark.dal.dataobject.BenchmarkTaskDO; import cn.iocoder.yudao.module.tik.benchmark.dal.mysql.BenchmarkTaskMapper; import cn.iocoder.yudao.module.tik.benchmark.enums.BenchmarkTaskStatusEnum; import cn.iocoder.yudao.module.tik.benchmark.vo.BenchmarkTaskCreateReqVO; +import cn.iocoder.yudao.module.tik.dify.service.DifyService; +import cn.iocoder.yudao.module.tik.dify.vo.DifyBenchmarkReqVO; +import cn.iocoder.yudao.module.tik.dify.vo.DifyChatRespVO; import cn.iocoder.yudao.module.tik.tikhup.service.TikHupService; import cn.iocoder.yudao.module.tik.userprompt.service.UserPromptService; import cn.iocoder.yudao.module.tik.userprompt.vo.UserPromptSaveReqVO; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Async; @@ -17,6 +24,7 @@ import org.springframework.stereotype.Component; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicReference; /** * 对标分析任务异步执行器 @@ -31,13 +39,16 @@ public class BenchmarkTaskExecutor { @Resource private TikHupService tikHupService; + @Resource + private DifyService difyService; + @Resource private UserPromptService userPromptService; /** * 异步执行对标分析任务 */ - @Async("benchmarkTaskExecutor") + @Async("benchmarkAsyncExecutor") public void executeAsync(Long taskId, BenchmarkTaskCreateReqVO createReqVO) { log.info("[executeAsync][taskId={}] 开始异步执行对标分析任务", taskId); @@ -51,35 +62,28 @@ public class BenchmarkTaskExecutor { // 1. 更新状态为执行中 updateTaskProgress(taskId, 0, BenchmarkTaskStatusEnum.RUNNING.getStatus()); - // 2. 获取视频转写 + // 2. 批量获取视频转写(使用 DashScope API) updateTaskProgress(taskId, 10, BenchmarkTaskStatusEnum.RUNNING.getStatus()); List videoUrls = createReqVO.getVideoUrls(); List transcriptions = new ArrayList<>(); - // 批量获取转写(使用 videoToCharacters2) try { + updateTaskProgress(taskId, 30, BenchmarkTaskStatusEnum.RUNNING.getStatus()); Object result = tikHupService.videoToCharacters2(videoUrls); - if (result instanceof List) { - List resultList = (List) result; - for (Object item : resultList) { - if (item != null && StrUtil.isNotBlank(item.toString())) { - transcriptions.add(item.toString()); - } - } - } - } catch (Exception e) { - log.warn("[executeAsync][taskId={}] 批量转写失败,尝试逐个转写: {}", taskId, e.getMessage()); - // 降级:逐个转写 - for (String videoUrl : videoUrls) { - try { - Object result = tikHupService.videoToCharacters(videoUrl); - if (result != null && StrUtil.isNotBlank(result.toString())) { - transcriptions.add(result.toString()); - } - } catch (Exception ex) { - log.warn("[executeAsync][taskId={}, videoUrl={}] 转写失败: {}", taskId, videoUrl, ex.getMessage()); + log.info("[executeAsync][taskId={}] videoToCharacters2 返回类型: {}", + taskId, result != null ? result.getClass().getName() : "null"); + + if (result instanceof CommonResult) { + CommonResult commonResult = (CommonResult) result; + if (commonResult.getData() != null) { + String rawData = commonResult.getData().toString(); + transcriptions = extractBatchTranscriptionTexts(rawData); + log.info("[executeAsync][taskId={}] 批量转写成功,视频数: {}, 结果数: {}", + taskId, videoUrls.size(), transcriptions.size()); } } + } catch (Exception ex) { + log.warn("[executeAsync][taskId={}] 批量转写失败: {}", taskId, ex.getMessage()); } updateTaskProgress(taskId, 60, BenchmarkTaskStatusEnum.RUNNING.getStatus()); @@ -99,13 +103,39 @@ public class BenchmarkTaskExecutor { updateTaskProgress(taskId, 70, BenchmarkTaskStatusEnum.RUNNING.getStatus()); - // 5. 调用 AI 生成提示词 + // 5. 调用 Dify 生成提示词(流式) String generatedPrompt; try { - Object result = tikHupService.deepseekAnalysis("benchmark", mergedText); - generatedPrompt = result != null ? result.toString() : ""; + DifyBenchmarkReqVO difyReq = new DifyBenchmarkReqVO(); + difyReq.setContent(mergedText); + difyReq.setVideoCount(transcriptions.size()); + + // 收集流式响应 + StringBuilder resultBuilder = new StringBuilder(); + AtomicReference errorMsgRef = new AtomicReference<>(); + + difyService.benchmarkAnalyzeStream(difyReq, String.valueOf(task.getUserId())) + .filter(resp -> resp != null) + .doOnNext(resp -> { + if (DifyChatRespVO.EVENT_MESSAGE.equals(resp.getEvent()) + && StrUtil.isNotBlank(resp.getContent())) { + resultBuilder.append(resp.getContent()); + } else if (DifyChatRespVO.EVENT_ERROR.equals(resp.getEvent())) { + errorMsgRef.set(resp.getErrorMessage()); + } + }) + .blockLast(); + + String errorMsg = errorMsgRef.get(); + if (errorMsg != null) { + throw new RuntimeException(errorMsg); + } + + generatedPrompt = resultBuilder.toString(); + log.info("[executeAsync][taskId={}] Dify 分析完成,生成内容长度: {}", taskId, generatedPrompt.length()); + } catch (Exception e) { - log.warn("[executeAsync][taskId={}] AI 生成失败,使用默认提示词: {}", taskId, e.getMessage()); + log.warn("[executeAsync][taskId={}] Dify 分析失败,使用默认提示词: {}", taskId, e.getMessage()); // 降级:使用合并文本的前2000字符 generatedPrompt = "请参考以下视频文案风格生成内容:\n\n" + mergedText.substring(0, Math.min(2000, mergedText.length())); @@ -191,4 +221,157 @@ public class BenchmarkTaskExecutor { return userPromptService.createUserPrompt(saveReqVO); } + /** + * 从阿里云转写结果中提取纯文本 + * 支持多种 JSON 格式: + * 1. {"sentences": [{"text": "xxx"}, ...]} + * 2. {"transcripts": [{"text": "xxx"}]} + * 3. 纯文本 + */ + private String extractTranscriptionText(String rawData) { + if (StrUtil.isBlank(rawData)) { + return null; + } + + try { + JSONObject json = JSON.parseObject(rawData); + + // 格式1&2: sentences 或 transcripts 数组 + String result = extractFromArray(json.getJSONArray("sentences"), "text"); + if (result == null) { + result = extractFromArray(json.getJSONArray("transcripts"), "text"); + } + if (result != null) { + return result; + } + + // 格式3: 直接的 text 字段 + String text = json.getString("text"); + if (StrUtil.isNotBlank(text)) { + return text; + } + + // 格式4: Result 字段嵌套 + String resultField = json.getString("Result"); + if (StrUtil.isNotBlank(resultField)) { + return extractTranscriptionText(resultField); + } + + log.warn("[extractTranscriptionText] 未知的 JSON 格式: {}", rawData.substring(0, Math.min(200, rawData.length()))); + } catch (Exception e) { + log.info("[extractTranscriptionText] 非JSON格式,直接返回原文,长度: {}", rawData.length()); + return rawData; + } + return null; + } + + /** + * 从 JSON 数组中提取文本 + */ + private String extractFromArray(JSONArray array, String textField) { + if (array == null || array.isEmpty()) { + return null; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < array.size(); i++) { + Object item = array.get(i); + String text = null; + if (item instanceof JSONObject) { + text = ((JSONObject) item).getString(textField); + } else if (item != null) { + text = item.toString(); + } + if (StrUtil.isNotBlank(text)) { + if (sb.length() > 0) { + sb.append(" "); + } + sb.append(text); + } + } + return sb.length() > 0 ? sb.toString() : null; + } + + /** + * 从 DashScope 批量转写结果中提取所有文本 + * 格式: {"results":[{"transcription_url":"https://..."},...]} + */ + private List extractBatchTranscriptionTexts(String rawData) { + List results = new ArrayList<>(); + if (StrUtil.isBlank(rawData)) { + return results; + } + + try { + JSONObject json = JSON.parseObject(rawData); + JSONArray resultsArray = json.getJSONArray("results"); + if (resultsArray == null || resultsArray.isEmpty()) { + log.warn("[extractBatchTranscriptionTexts] 未找到 results 数组"); + return results; + } + + for (int i = 0; i < resultsArray.size(); i++) { + JSONObject item = resultsArray.getJSONObject(i); + String transcriptionUrl = item.getString("transcription_url"); + if (StrUtil.isBlank(transcriptionUrl)) { + continue; + } + + try { + // 获取转写结果 URL 的内容 + String transcriptionJson = fetchUrlContent(transcriptionUrl); + if (StrUtil.isBlank(transcriptionJson)) { + continue; + } + + // 解析转写内容 + JSONObject transJson = JSON.parseObject(transcriptionJson); + JSONArray transcripts = transJson.getJSONArray("transcripts"); + if (transcripts != null && !transcripts.isEmpty()) { + JSONObject firstTranscript = transcripts.getJSONObject(0); + String text = firstTranscript.getString("text"); + if (StrUtil.isNotBlank(text)) { + results.add(text); + log.info("[extractBatchTranscriptionTexts][{}/{}] 提取成功,长度: {}", + i + 1, resultsArray.size(), text.length()); + } + } + } catch (Exception ex) { + log.warn("[extractBatchTranscriptionTexts][{}] 获取转写内容失败: {}", i, ex.getMessage()); + } + } + } catch (Exception e) { + log.error("[extractBatchTranscriptionTexts] 解析失败: {}", e.getMessage()); + } + + return results; + } + + /** + * 获取 URL 内容 + */ + private String fetchUrlContent(String url) { + try { + java.net.HttpURLConnection conn = (java.net.HttpURLConnection) new java.net.URL(url).openConnection(); + conn.setConnectTimeout(10000); + conn.setReadTimeout(30000); + conn.setRequestMethod("GET"); + + if (conn.getResponseCode() == 200) { + try (java.io.InputStream is = conn.getInputStream(); + java.io.BufferedReader reader = new java.io.BufferedReader( + new java.io.InputStreamReader(is, java.nio.charset.StandardCharsets.UTF_8))) { + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line); + } + return sb.toString(); + } + } + } catch (Exception e) { + log.warn("[fetchUrlContent] 获取 URL 内容失败: {}, error: {}", url, e.getMessage()); + } + return null; + } + } diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/config/TikAsyncConfig.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/config/TikAsyncConfig.java index daf78841e6..50d83f4401 100644 --- a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/config/TikAsyncConfig.java +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/config/TikAsyncConfig.java @@ -7,12 +7,11 @@ import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionHandler; import java.util.concurrent.ThreadPoolExecutor; /** * Tik模块异步任务配置 - 限流保护 - * - * @author 芋道源码 */ @Slf4j @Configuration @@ -20,83 +19,42 @@ import java.util.concurrent.ThreadPoolExecutor; public class TikAsyncConfig { /** - * 数字人任务专用线程池 - 限流保护 + * 创建线程池执行器 */ - @Bean("taskExecutor") - public Executor taskExecutor() { + private Executor createExecutor(String prefix, int coreSize, int maxSize, int queueCapacity, + int keepAlive, RejectedExecutionHandler handler) { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); - - // 核心线程数:10(CPU核心数) - executor.setCorePoolSize(10); - - // 最大线程数:20 - executor.setMaxPoolSize(20); - - // 队列长度:50(超过的请求会被拒绝) - executor.setQueueCapacity(50); - - // 线程名前缀 - executor.setThreadNamePrefix("digital-human-"); - - // 线程空闲时间:60秒 - executor.setKeepAliveSeconds(60); - - // 关闭时等待任务完成 + executor.setCorePoolSize(coreSize); + executor.setMaxPoolSize(maxSize); + executor.setQueueCapacity(queueCapacity); + executor.setThreadNamePrefix(prefix); + executor.setKeepAliveSeconds(keepAlive); executor.setWaitForTasksToCompleteOnShutdown(true); - - // 等待时间:60秒 - executor.setAwaitTerminationSeconds(60); - - // 拒绝策略:抛出异常(也可以改为 CallerRunsPolicy 回退到调用者线程) - executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy()); - - // 初始化 + executor.setAwaitTerminationSeconds(keepAlive); + executor.setRejectedExecutionHandler(handler); executor.initialize(); - - log.info("[TikAsyncConfig][数字人任务线程池已初始化][corePoolSize={}, maxPoolSize={}, queueCapacity={}]", - executor.getCorePoolSize(), executor.getMaxPoolSize(), executor.getQueueCapacity()); - return executor; } - /** - * Latentsync轮询专用线程池 - 轻量化处理 - */ + /** 数字人任务专用线程池 */ + @Bean("taskExecutor") + public Executor taskExecutor() { + return createExecutor("digital-human-", 10, 20, 50, 60, + new ThreadPoolExecutor.AbortPolicy()); + } + + /** Latentsync轮询专用线程池 */ @Bean("latentsyncPollingExecutor") public Executor latentsyncPollingExecutor() { - ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + return createExecutor("latentsync-poll-", 5, 10, 100, 30, + new ThreadPoolExecutor.DiscardOldestPolicy()); + } - // 核心线程数:5(轮询任务不需要太多线程) - executor.setCorePoolSize(5); - - // 最大线程数:10 - executor.setMaxPoolSize(10); - - // 队列长度:100 - executor.setQueueCapacity(100); - - // 线程名前缀 - executor.setThreadNamePrefix("latentsync-poll-"); - - // 线程空闲时间:30秒 - executor.setKeepAliveSeconds(30); - - // 关闭时等待任务完成 - executor.setWaitForTasksToCompleteOnShutdown(true); - - // 等待时间:30秒 - executor.setAwaitTerminationSeconds(30); - - // 拒绝策略:直接丢弃最旧的任务 - executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy()); - - // 初始化 - executor.initialize(); - - log.info("[TikAsyncConfig][Latentsync轮询线程池已初始化][corePoolSize={}, maxPoolSize={}, queueCapacity={}]", - executor.getCorePoolSize(), executor.getMaxPoolSize(), executor.getQueueCapacity()); - - return executor; + /** 对标分析任务专用线程池 */ + @Bean("benchmarkAsyncExecutor") + public Executor benchmarkAsyncExecutor() { + return createExecutor("benchmark-task-", 5, 10, 30, 60, + new ThreadPoolExecutor.AbortPolicy()); } } diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/tikhup/controller/TikHupController.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/tikhup/controller/TikHupController.java index d6a74ff7bf..2147894551 100644 --- a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/tikhup/controller/TikHupController.java +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/tikhup/controller/TikHupController.java @@ -32,13 +32,6 @@ public class TikHupController { return tikHupService.postTikHup(type, methodType, urlParams, paramType); } - @PostMapping("/videoToCharacters") - @Operation(summary = "音频转文字", description = "音频转文字接口") - public Object videoToCharacters(@RequestBody Map fileLinkMap) { - String fileLink = (String) fileLinkMap.get("fileLink"); - return tikHupService.videoToCharacters(fileLink); - } - @PostMapping("/videoToCharacters2") @Operation(summary = "批量音频转文字", description = "批量音频转文字接口") public Object videoToCharacters2(@RequestBody Map fileLinkMap) { diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/tikhup/service/TikHupService.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/tikhup/service/TikHupService.java index 376e8ee2a1..0de26fd610 100644 --- a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/tikhup/service/TikHupService.java +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/tikhup/service/TikHupService.java @@ -9,13 +9,6 @@ public interface TikHupService { Object postTikHup(String type, String methodType, String urlParams, String paramType); - /** - * 音频转文字 - * @param downloadUrl 下载地址 - * @return 语音文字 - */ - Object videoToCharacters(String downloadUrl); - /** * 批量音频转文字 * @param fileLinkList 音频地址 diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/tikhup/service/TikHupServiceImpl.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/tikhup/service/TikHupServiceImpl.java index 513c8c1711..d3dd0d4412 100644 --- a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/tikhup/service/TikHupServiceImpl.java +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/tikhup/service/TikHupServiceImpl.java @@ -51,7 +51,6 @@ public class TikHupServiceImpl implements TikHupService { // 日志前缀 private static final String LOG_PREFIX_TIKHUP = "[postTikHup]"; - private static final String LOG_PREFIX_VIDEO = "[videoToCharacters]"; private static final String LOG_PREFIX_VIDEO2 = "[videoToCharacters2]"; private static final String LOG_PREFIX_DEEPSEEK = "[deepseekAnalysis]"; @@ -66,11 +65,6 @@ public class TikHupServiceImpl implements TikHupService { private static final String DEEPSEEK_API_KEY = "sk-7f666f993b144d279ae375a015e4de56"; private static final String DEEPSEEK_API_URL = "https://api.deepseek.com/chat/completions"; - // 阿里云语音识别配置 - private final String appKey = "sldJ4XSpYp3rKALZ"; - private final String accessKeyId = "LTAI5tPV9Ag3csf41GZjaLTA"; - private final String accessKeySecret = "kDqlGeJTKw6tJtFYiaY8vQTFuVIQDs"; - // 流式处理配置 private static final Duration STREAM_TIMEOUT = Duration.ofMinutes(30); @@ -269,29 +263,6 @@ public class TikHupServiceImpl implements TikHupService { } } - @Override - public Object videoToCharacters(String fileLink) { - log.info("{} 开始识别,文件链接: {}", LOG_PREFIX_VIDEO, fileLink); - TikFileTransCharacters transCharacters = new TikFileTransCharacters(accessKeyId, accessKeySecret); - String taskId = transCharacters.submitFileTransRequest(appKey, fileLink); - - if (taskId == null) { - log.error("{} 提交识别请求失败,taskId为null,fileLink: {}", LOG_PREFIX_VIDEO, fileLink); - return CommonResult.error(500, "录音文件识别请求失败!"); - } - - log.info("{} 提交识别请求成功,taskId: {}", LOG_PREFIX_VIDEO, taskId); - String transResult = transCharacters.getFileTransResult(taskId); - - if (transResult == null) { - log.error("{} 识别结果查询失败,taskId: {}", LOG_PREFIX_VIDEO, taskId); - return CommonResult.error(501, "录音文件识别请求失败!"); - } - - log.info("{} 识别成功,taskId: {},结果长度: {}", LOG_PREFIX_VIDEO, taskId, transResult.length()); - return CommonResult.success(transResult); - } - @Override public Object videoToCharacters2(List fileLinkList) { log.info("{} 开始识别,文件数量: {},文件URL: {}",