yumeng 2 dienas atpakaļ
vecāks
revīzija
e60c23ea22

+ 37 - 0
src/main/java/com/video/script/admin/config/LlmProperties.java

@@ -18,6 +18,7 @@ public class LlmProperties {
     private int timeoutMs = 180000;
     private boolean multimodalEnabled = true;
     private boolean sendVideoUrls = true;
+    private OmniProperties omni = new OmniProperties();
 
     public boolean isEnabled() { return enabled; }
     public void setEnabled(boolean enabled) { this.enabled = enabled; }
@@ -41,4 +42,40 @@ public class LlmProperties {
     public void setMultimodalEnabled(boolean multimodalEnabled) { this.multimodalEnabled = multimodalEnabled; }
     public boolean isSendVideoUrls() { return sendVideoUrls; }
     public void setSendVideoUrls(boolean sendVideoUrls) { this.sendVideoUrls = sendVideoUrls; }
+    public OmniProperties getOmni() { return omni; }
+    public void setOmni(OmniProperties omni) { this.omni = omni; }
+
+    public static class OmniProperties {
+        private boolean enabled;
+        private String provider = "qwen";
+        private String baseUrl;
+        private String endpoint = "/chat/completions";
+        private String apiKey;
+        private String model = "qwen3.5-omni-plus";
+        private String authHeader = "Authorization";
+        private String authPrefix = "Bearer ";
+        private int timeoutMs = 180000;
+        private boolean sendVideoUrls = true;
+
+        public boolean isEnabled() { return enabled; }
+        public void setEnabled(boolean enabled) { this.enabled = enabled; }
+        public String getProvider() { return provider; }
+        public void setProvider(String provider) { this.provider = provider; }
+        public String getBaseUrl() { return baseUrl; }
+        public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; }
+        public String getEndpoint() { return endpoint; }
+        public void setEndpoint(String endpoint) { this.endpoint = endpoint; }
+        public String getApiKey() { return apiKey; }
+        public void setApiKey(String apiKey) { this.apiKey = apiKey; }
+        public String getModel() { return model; }
+        public void setModel(String model) { this.model = model; }
+        public String getAuthHeader() { return authHeader; }
+        public void setAuthHeader(String authHeader) { this.authHeader = authHeader; }
+        public String getAuthPrefix() { return authPrefix; }
+        public void setAuthPrefix(String authPrefix) { this.authPrefix = authPrefix; }
+        public int getTimeoutMs() { return timeoutMs; }
+        public void setTimeoutMs(int timeoutMs) { this.timeoutMs = timeoutMs; }
+        public boolean isSendVideoUrls() { return sendVideoUrls; }
+        public void setSendVideoUrls(boolean sendVideoUrls) { this.sendVideoUrls = sendVideoUrls; }
+    }
 }

+ 2 - 1
src/main/java/com/video/script/admin/config/RestTemplateConfig.java

@@ -11,7 +11,8 @@ public class RestTemplateConfig {
 
     @Bean
     public RestTemplate restTemplate(RestTemplateBuilder builder, LlmProperties llmProperties, AsrProperties asrProperties) {
-        int timeoutMs = Math.max(llmProperties.getTimeoutMs(), asrProperties.getTimeoutMs());
+        int timeoutMs = Math.max(Math.max(llmProperties.getTimeoutMs(), asrProperties.getTimeoutMs()),
+                llmProperties.getOmni().getTimeoutMs());
         return builder
                 .setConnectTimeout(Duration.ofMillis(timeoutMs))
                 .setReadTimeout(Duration.ofMillis(timeoutMs))

+ 6 - 0
src/main/java/com/video/script/admin/dto/TaskSaveRequest.java

@@ -21,10 +21,12 @@ public class TaskSaveRequest {
     private String promptHint;
     private String transcript;
     private String videoContent;
+    private String omniContent;
     private String analysisContent;
     private String scriptContent;
     private String summary;
     private String codedeskTaskId;
+    private Boolean useOmniAnalysis;
 
     public Long getId() { return id; }
     public void setId(Long id) { this.id = id; }
@@ -44,6 +46,8 @@ public class TaskSaveRequest {
     public void setTranscript(String transcript) { this.transcript = transcript; }
     public String getVideoContent() { return videoContent; }
     public void setVideoContent(String videoContent) { this.videoContent = videoContent; }
+    public String getOmniContent() { return omniContent; }
+    public void setOmniContent(String omniContent) { this.omniContent = omniContent; }
     public String getAnalysisContent() { return analysisContent; }
     public void setAnalysisContent(String analysisContent) { this.analysisContent = analysisContent; }
     public String getScriptContent() { return scriptContent; }
@@ -52,4 +56,6 @@ public class TaskSaveRequest {
     public void setSummary(String summary) { this.summary = summary; }
     public String getCodedeskTaskId() { return codedeskTaskId; }
     public void setCodedeskTaskId(String codedeskTaskId) { this.codedeskTaskId = codedeskTaskId; }
+    public Boolean getUseOmniAnalysis() { return useOmniAnalysis; }
+    public void setUseOmniAnalysis(Boolean useOmniAnalysis) { this.useOmniAnalysis = useOmniAnalysis; }
 }

+ 6 - 0
src/main/java/com/video/script/admin/entity/VideoScriptTask.java

@@ -11,10 +11,12 @@ public class VideoScriptTask {
     private String promptHint;
     private String transcript;
     private String videoContent;
+    private String omniContent;
     private String analysisContent;
     private String scriptContent;
     private String summary;
     private String codedeskTaskId;
+    private Boolean useOmniAnalysis;
     private String errorMessage;
     private Long creatorId;
     private String creatorName;
@@ -39,6 +41,8 @@ public class VideoScriptTask {
     public void setTranscript(String transcript) { this.transcript = transcript; }
     public String getVideoContent() { return videoContent; }
     public void setVideoContent(String videoContent) { this.videoContent = videoContent; }
+    public String getOmniContent() { return omniContent; }
+    public void setOmniContent(String omniContent) { this.omniContent = omniContent; }
     public String getAnalysisContent() { return analysisContent; }
     public void setAnalysisContent(String analysisContent) { this.analysisContent = analysisContent; }
     public String getScriptContent() { return scriptContent; }
@@ -47,6 +51,8 @@ public class VideoScriptTask {
     public void setSummary(String summary) { this.summary = summary; }
     public String getCodedeskTaskId() { return codedeskTaskId; }
     public void setCodedeskTaskId(String codedeskTaskId) { this.codedeskTaskId = codedeskTaskId; }
+    public Boolean getUseOmniAnalysis() { return useOmniAnalysis; }
+    public void setUseOmniAnalysis(Boolean useOmniAnalysis) { this.useOmniAnalysis = useOmniAnalysis; }
     public String getErrorMessage() { return errorMessage; }
     public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; }
     public Long getCreatorId() { return creatorId; }

+ 1 - 0
src/main/java/com/video/script/admin/mapper/VideoScriptTaskMapper.java

@@ -26,6 +26,7 @@ public interface VideoScriptTaskMapper {
                              @Param("status") String status,
                              @Param("transcript") String transcript,
                              @Param("videoContent") String videoContent,
+                             @Param("omniContent") String omniContent,
                              @Param("analysisContent") String analysisContent,
                              @Param("scriptContent") String scriptContent,
                              @Param("summary") String summary,

+ 8 - 0
src/main/java/com/video/script/admin/service/VideoAnalysisClient.java

@@ -0,0 +1,8 @@
+package com.video.script.admin.service;
+
+import com.video.script.admin.entity.VideoScriptTask;
+
+public interface VideoAnalysisClient {
+
+    String analyze(VideoScriptTask task);
+}

+ 20 - 0
src/main/java/com/video/script/admin/service/impl/OpenAiCompatibleLlmClient.java

@@ -73,6 +73,21 @@ public class OpenAiCompatibleLlmClient implements LlmScriptClient {
 
         String url = normalizeBaseUrl(properties.getBaseUrl()) + normalizePath(properties.getEndpoint());
         try {
+            log.info("qwen3.7-plus inputs, taskId={}, taskName={}, videoCount={}, transcriptLength={}, videoContentLength={}, omniContentLength={}, promptHintLength={}",
+                    task.getId(),
+                    task.getTaskName(),
+                    videoCount(task),
+                    StringUtils.length(task.getTranscript()),
+                    StringUtils.length(task.getVideoContent()),
+                    StringUtils.length(task.getOmniContent()),
+                    StringUtils.length(resolvePromptHint(task)));
+            log.info("qwen3.7-plus input content, taskId={}, taskName={}, promptHint=\n{}\n\nvideoContent=\n{}\n\nomniContent=\n{}\n\ntranscript=\n{}",
+                    task.getId(),
+                    task.getTaskName(),
+                    defaultText(resolvePromptHint(task)),
+                    defaultText(task.getVideoContent()),
+                    defaultText(task.getOmniContent()),
+                    defaultText(task.getTranscript()));
             logLlmRequest(task, url, body);
             log.info("Calling LLM provider={}, url={}, model={}, authHeader={}, apiKey={}",
                     properties.getProvider(),
@@ -121,6 +136,7 @@ public class OpenAiCompatibleLlmClient implements LlmScriptClient {
                 .append("视频数量:").append(videoCount > 0 ? videoCount : 1).append("\n\n")
                 .append("用户要求:\n").append(defaultText(resolvePromptHint(task))).append("\n\n")
                 .append("视频结构化内容:\n").append(defaultText(task.getVideoContent())).append("\n\n")
+                .append("Omni 视频分析:\n").append(defaultText(task.getOmniContent())).append("\n\n")
                 .append("转写文本:\n").append(defaultText(task.getTranscript())).append("\n\n")
                 .append("脚本产出要求:\n").append(buildScriptTypeInstruction(task.getScriptType())).append("\n")
                 .append("分析文案要求:\n")
@@ -294,6 +310,10 @@ public class OpenAiCompatibleLlmClient implements LlmScriptClient {
         return normalizedPrefix + " " + StringUtils.trim(apiKey);
     }
 
+    private int videoCount(VideoScriptTask task) {
+        return resolveVideoUrls(task).size();
+    }
+
     private String maskKey(String apiKey) {
         String value = StringUtils.trimToEmpty(apiKey);
         if (value.length() <= 10) {

+ 193 - 0
src/main/java/com/video/script/admin/service/impl/QwenOmniVideoAnalysisClient.java

@@ -0,0 +1,193 @@
+package com.video.script.admin.service.impl;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.video.script.admin.common.BusinessException;
+import com.video.script.admin.config.LlmProperties;
+import com.video.script.admin.entity.VideoScriptTask;
+import com.video.script.admin.service.VideoAnalysisClient;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestClientException;
+import org.springframework.web.client.RestTemplate;
+
+@Component
+public class QwenOmniVideoAnalysisClient implements VideoAnalysisClient {
+
+    private static final Logger log = LoggerFactory.getLogger(QwenOmniVideoAnalysisClient.class);
+
+    private static final String SYSTEM_PROMPT =
+            "你是短视频多模态分析助手。请基于视频画面、音频线索、已有转写和素材顺序,输出适合后续广告脚本改编的结构化中文分析。"
+                    + "必须重点提炼每条视频的开场钩子、人物/场景、动作演示、卖点表达、情绪变化、证明信息、字幕信息、结尾引导。"
+                    + "如果多个视频存在相似表达,要合并共性并指出差异。不要输出 markdown 表格,不要编造不存在的信息。";
+
+    private final RestTemplate restTemplate;
+    private final LlmProperties properties;
+    private final ObjectMapper objectMapper;
+
+    public QwenOmniVideoAnalysisClient(RestTemplate restTemplate, LlmProperties properties, ObjectMapper objectMapper) {
+        this.restTemplate = restTemplate;
+        this.properties = properties;
+        this.objectMapper = objectMapper;
+    }
+
+    @Override
+    public String analyze(VideoScriptTask task) {
+        LlmProperties.OmniProperties omni = properties.getOmni();
+        if (!omni.isEnabled()) {
+            throw new BusinessException("Omni 分析未启用");
+        }
+        if (StringUtils.isAnyBlank(omni.getBaseUrl(), omni.getEndpoint(), omni.getApiKey(), omni.getModel())) {
+            throw new BusinessException("Omni 配置不完整");
+        }
+        List<String> videoUrls = resolveVideoUrls(task);
+        if (videoUrls.isEmpty()) {
+            throw new BusinessException("Omni 分析缺少 videoUrls");
+        }
+
+        Map<String, Object> body = new LinkedHashMap<>();
+        body.put("model", omni.getModel());
+        body.put("messages", buildMessages(task, videoUrls, omni.isSendVideoUrls()));
+        body.put("stream", false);
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_JSON);
+        headers.set(omni.getAuthHeader(), buildAuthorizationValue(omni.getAuthPrefix(), omni.getApiKey()));
+
+        String url = normalizeBaseUrl(omni.getBaseUrl()) + normalizePath(omni.getEndpoint());
+        try {
+            log.info("Calling Omni provider={}, url={}, model={}, taskId={}, taskName={}",
+                    omni.getProvider(), url, omni.getModel(), task.getId(), task.getTaskName());
+            String rawResponse = restTemplate.postForObject(url, new HttpEntity<>(body, headers), String.class);
+            String analysis = parseResponse(rawResponse);
+            log.info("Omni analysis result, taskId={}, taskName={}, content=\n{}",
+                    task.getId(),
+                    task.getTaskName(),
+                    analysis);
+            return analysis;
+        } catch (RestClientException ex) {
+            throw new BusinessException("Omni 分析调用失败: " + ex.getMessage());
+        }
+    }
+
+    private List<Map<String, Object>> buildMessages(VideoScriptTask task, List<String> videoUrls, boolean sendVideoUrls) {
+        List<Map<String, Object>> messages = new ArrayList<>();
+        messages.add(message("system", SYSTEM_PROMPT));
+        StringBuilder userPrompt = new StringBuilder();
+        userPrompt.append("任务名称:").append(defaultText(task.getTaskName())).append("\n")
+                .append("脚本类型:").append(defaultText(task.getScriptType())).append("\n")
+                .append("视频数量:").append(videoUrls.size()).append("\n\n")
+                .append("已有 ASR 转写:\n").append(defaultText(task.getTranscript())).append("\n\n")
+                .append("已有 ASR 结构化信息:\n").append(defaultText(task.getVideoContent())).append("\n\n")
+                .append("输出要求:\n")
+                .append("1. 逐条视频输出关键信息,建议至少包含【视频编号】【开场钩子】【核心画面】【人物/场景】【口播/字幕重点】【卖点】【证明点】【情绪/节奏】【结尾动作】。\n")
+                .append("2. 如果多个视频有共性,再补充【跨素材共性】和【差异亮点】。\n")
+                .append("3. 最后补充【脚本改编建议】,说明后续广告脚本最值得保留和重组的元素。\n")
+                .append("4. 输出纯文本结构化内容,不要 JSON,不要 markdown 代码块。");
+        if (sendVideoUrls) {
+            messages.add(multimodalUserMessage(userPrompt.toString(), videoUrls));
+        } else {
+            messages.add(message("user", userPrompt.toString()));
+        }
+        return messages;
+    }
+
+    private String parseResponse(String rawResponse) {
+        if (StringUtils.isBlank(rawResponse)) {
+            throw new BusinessException("Omni 返回为空");
+        }
+        try {
+            JsonNode root = objectMapper.readTree(rawResponse);
+            String content = root.path("choices").path(0).path("message").path("content").asText(null);
+            if (StringUtils.isBlank(content)) {
+                throw new BusinessException("Omni 返回内容为空");
+            }
+            return content.trim();
+        } catch (BusinessException ex) {
+            throw ex;
+        } catch (Exception ex) {
+            throw new BusinessException("解析 Omni 响应失败: " + ex.getMessage());
+        }
+    }
+
+    private Map<String, Object> message(String role, String content) {
+        Map<String, Object> message = new LinkedHashMap<>();
+        message.put("role", role);
+        message.put("content", content);
+        return message;
+    }
+
+    private Map<String, Object> multimodalUserMessage(String prompt, List<String> videoUrls) {
+        Map<String, Object> message = new LinkedHashMap<>();
+        message.put("role", "user");
+        List<Map<String, Object>> content = new ArrayList<>();
+        content.add(contentText(prompt));
+        for (String videoUrl : videoUrls) {
+            content.add(contentVideo(videoUrl));
+        }
+        message.put("content", content);
+        return message;
+    }
+
+    private Map<String, Object> contentText(String text) {
+        Map<String, Object> item = new LinkedHashMap<>();
+        item.put("type", "text");
+        item.put("text", text);
+        return item;
+    }
+
+    private Map<String, Object> contentVideo(String videoUrl) {
+        Map<String, Object> item = new LinkedHashMap<>();
+        item.put("type", "video_url");
+        item.put("video_url", jsonObject("url", videoUrl));
+        return item;
+    }
+
+    private Map<String, String> jsonObject(String key, String value) {
+        Map<String, String> map = new LinkedHashMap<>();
+        map.put(key, value);
+        return map;
+    }
+
+    private List<String> resolveVideoUrls(VideoScriptTask task) {
+        if (StringUtils.isNotBlank(task.getVideoUrls())) {
+            return Arrays.stream(task.getVideoUrls().split("\\r?\\n|,"))
+                    .filter(StringUtils::isNotBlank)
+                    .map(String::trim)
+                    .distinct()
+                    .collect(Collectors.toList());
+        }
+        if (StringUtils.isNotBlank(task.getVideoUrl())) {
+            return java.util.Collections.singletonList(task.getVideoUrl().trim());
+        }
+        return java.util.Collections.emptyList();
+    }
+
+    private String normalizeBaseUrl(String baseUrl) {
+        return StringUtils.removeEnd(baseUrl, "/");
+    }
+
+    private String normalizePath(String path) {
+        return path.startsWith("/") ? path : "/" + path;
+    }
+
+    private String buildAuthorizationValue(String prefix, String apiKey) {
+        String normalizedPrefix = StringUtils.defaultIfBlank(prefix, "Bearer").trim();
+        return normalizedPrefix + " " + StringUtils.trim(apiKey);
+    }
+
+    private String defaultText(String text) {
+        return StringUtils.defaultIfBlank(text, "无");
+    }
+}

+ 91 - 0
src/main/java/com/video/script/admin/service/impl/TaskServiceImpl.java

@@ -9,31 +9,39 @@ import com.video.script.admin.entity.VideoScriptTask;
 import com.video.script.admin.mapper.VideoScriptTaskMapper;
 import com.video.script.admin.service.LlmScriptClient;
 import com.video.script.admin.service.TaskService;
+import com.video.script.admin.service.VideoAnalysisClient;
 import com.video.script.admin.service.VideoTranscriptionClient;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.stream.Collectors;
 import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Service;
 
 @Service
 public class TaskServiceImpl implements TaskService {
 
+    private static final Logger log = LoggerFactory.getLogger(TaskServiceImpl.class);
+
     private final VideoScriptTaskMapper videoScriptTaskMapper;
     private final LlmScriptClient llmScriptClient;
     private final LlmProperties llmProperties;
     private final VideoTranscriptionClient videoTranscriptionClient;
+    private final VideoAnalysisClient videoAnalysisClient;
     private final AsrProperties asrProperties;
 
     public TaskServiceImpl(VideoScriptTaskMapper videoScriptTaskMapper,
                            LlmScriptClient llmScriptClient,
                            LlmProperties llmProperties,
                            VideoTranscriptionClient videoTranscriptionClient,
+                           VideoAnalysisClient videoAnalysisClient,
                            AsrProperties asrProperties) {
         this.videoScriptTaskMapper = videoScriptTaskMapper;
         this.llmScriptClient = llmScriptClient;
         this.llmProperties = llmProperties;
         this.videoTranscriptionClient = videoTranscriptionClient;
+        this.videoAnalysisClient = videoAnalysisClient;
         this.asrProperties = asrProperties;
     }
 
@@ -86,6 +94,12 @@ public class TaskServiceImpl implements TaskService {
         if (entity == null) {
             throw new BusinessException("任务不存在");
         }
+        String originalTaskName = entity.getTaskName();
+        String originalVideoUrl = entity.getVideoUrl();
+        String originalVideoUrls = entity.getVideoUrls();
+        String originalScriptType = entity.getScriptType();
+        String originalPromptHint = entity.getPromptHint();
+        Boolean originalUseOmniAnalysis = entity.getUseOmniAnalysis();
         entity.setId(request.getId());
         if (StringUtils.isNotBlank(request.getTaskName())) {
             entity.setTaskName(request.getTaskName());
@@ -111,6 +125,9 @@ public class TaskServiceImpl implements TaskService {
         if (request.getVideoContent() != null) {
             entity.setVideoContent(request.getVideoContent());
         }
+        if (request.getOmniContent() != null) {
+            entity.setOmniContent(request.getOmniContent());
+        }
         if (request.getAnalysisContent() != null) {
             entity.setAnalysisContent(request.getAnalysisContent());
         }
@@ -123,6 +140,20 @@ public class TaskServiceImpl implements TaskService {
         if (request.getCodedeskTaskId() != null) {
             entity.setCodedeskTaskId(StringUtils.trimToNull(request.getCodedeskTaskId()));
         }
+        if (request.getUseOmniAnalysis() != null) {
+            entity.setUseOmniAnalysis(request.getUseOmniAnalysis());
+        }
+        if (shouldRegenerate(request, entity, originalTaskName, originalVideoUrl, originalVideoUrls,
+                originalScriptType, originalPromptHint, originalUseOmniAnalysis)) {
+            entity.setStatus("PENDING");
+            entity.setTranscript(null);
+            entity.setVideoContent(null);
+            entity.setOmniContent(null);
+            entity.setAnalysisContent(null);
+            entity.setScriptContent(null);
+            entity.setSummary(null);
+            entity.setErrorMessage(null);
+        }
         videoScriptTaskMapper.update(entity);
     }
 
@@ -136,10 +167,12 @@ public class TaskServiceImpl implements TaskService {
         entity.setPromptHint(resolvePromptHint(request));
         entity.setTranscript(request.getTranscript());
         entity.setVideoContent(resolveInitialVideoContent(request));
+        entity.setOmniContent(StringUtils.trimToNull(request.getOmniContent()));
         entity.setAnalysisContent(request.getAnalysisContent());
         entity.setScriptContent(request.getScriptContent());
         entity.setSummary(request.getSummary());
         entity.setCodedeskTaskId(StringUtils.trimToNull(request.getCodedeskTaskId()));
+        entity.setUseOmniAnalysis(Boolean.TRUE.equals(request.getUseOmniAnalysis()));
         entity.setCreatorId(UserContext.getUserId());
         return entity;
     }
@@ -151,6 +184,7 @@ public class TaskServiceImpl implements TaskService {
                     "RUNNING",
                     entity.getTranscript(),
                     entity.getVideoContent(),
+                    entity.getOmniContent(),
                     entity.getAnalysisContent(),
                     entity.getScriptContent(),
                     entity.getSummary(),
@@ -169,6 +203,7 @@ public class TaskServiceImpl implements TaskService {
                         "RUNNING",
                         entity.getTranscript(),
                         entity.getVideoContent(),
+                        entity.getOmniContent(),
                         entity.getAnalysisContent(),
                         entity.getScriptContent(),
                         entity.getSummary(),
@@ -176,12 +211,37 @@ public class TaskServiceImpl implements TaskService {
                         null
                 );
             }
+            if (Boolean.TRUE.equals(entity.getUseOmniAnalysis())) {
+                String omniVideoContent = videoAnalysisClient.analyze(entity);
+                if (StringUtils.isNotBlank(omniVideoContent)) {
+                    entity.setOmniContent(omniVideoContent);
+                    log.info("Omni output assigned to qwen3.7-plus input, taskId={}, taskName={}, transcriptLength={}, videoContentLength={}, omniContentLength={}",
+                            entity.getId(),
+                            entity.getTaskName(),
+                            StringUtils.length(entity.getTranscript()),
+                            StringUtils.length(entity.getVideoContent()),
+                            StringUtils.length(entity.getOmniContent()));
+                    videoScriptTaskMapper.updateCallbackResult(
+                            entity.getId(),
+                            "RUNNING",
+                            entity.getTranscript(),
+                            entity.getVideoContent(),
+                            entity.getOmniContent(),
+                            entity.getAnalysisContent(),
+                            entity.getScriptContent(),
+                            entity.getSummary(),
+                            null,
+                            null
+                    );
+                }
+            }
             LlmScriptClient.ScriptGenerationResult result = llmScriptClient.generate(entity);
             videoScriptTaskMapper.updateCallbackResult(
                     entity.getId(),
                     "DONE",
                     entity.getTranscript(),
                     entity.getVideoContent(),
+                    entity.getOmniContent(),
                     result.getAnalysisContent(),
                     result.getScriptContent(),
                     result.getSummary(),
@@ -194,6 +254,7 @@ public class TaskServiceImpl implements TaskService {
                     "FAILED",
                     entity.getTranscript(),
                     entity.getVideoContent(),
+                    entity.getOmniContent(),
                     entity.getAnalysisContent(),
                     entity.getScriptContent(),
                     entity.getSummary(),
@@ -230,6 +291,36 @@ public class TaskServiceImpl implements TaskService {
         return request.getPromptHint() != null ? request.getVideoContent() : null;
     }
 
+    private boolean shouldRegenerate(TaskSaveRequest request,
+                                     VideoScriptTask entity,
+                                     String originalTaskName,
+                                     String originalVideoUrl,
+                                     String originalVideoUrls,
+                                     String originalScriptType,
+                                     String originalPromptHint,
+                                     Boolean originalUseOmniAnalysis) {
+        if (request.getId() == null) {
+            return false;
+        }
+        if (!StringUtils.equals(originalTaskName, entity.getTaskName())) {
+            return true;
+        }
+        if (!StringUtils.equals(originalVideoUrl, entity.getVideoUrl())) {
+            return true;
+        }
+        if (!StringUtils.equals(originalVideoUrls, entity.getVideoUrls())) {
+            return true;
+        }
+        if (!StringUtils.equals(originalScriptType, entity.getScriptType())) {
+            return true;
+        }
+        if (!StringUtils.equals(originalPromptHint, entity.getPromptHint())) {
+            return true;
+        }
+        return !java.util.Objects.equals(Boolean.TRUE.equals(originalUseOmniAnalysis),
+                Boolean.TRUE.equals(entity.getUseOmniAnalysis()));
+    }
+
     private void normalizeLegacyPromptHint(VideoScriptTask entity) {
         if (StringUtils.isBlank(entity.getPromptHint())
                 && StringUtils.isBlank(entity.getTranscript())

+ 11 - 0
src/main/resources/application-dev.yml

@@ -22,6 +22,17 @@ llm:
   timeout-ms: 180000
   multimodal-enabled: true
   send-video-urls: true
+  omni:
+    enabled: true
+    provider: qwen
+    base-url: "https://llm-ax2z3lqqt75hg7cu.cn-beijing.maas.aliyuncs.com/compatible-mode/v1"
+    endpoint: /chat/completions
+    api-key: "sk-ws-H.ERXPIHX.ITCw.MEYCIQD9fnRpudQRlLLQKL2faPTJejqa13VV5t2Q7pRGF-MV7wIhANeQ-QRx4eoJeTxy1QtV8B5lemcIzyr05s3fTMH5ARUO"
+    model: qwen3.5-omni-plus
+    auth-header: Authorization
+    auth-prefix: Bearer
+    timeout-ms: 180000
+    send-video-urls: true
 
 asr:
   enabled: true

+ 11 - 0
src/main/resources/application-prod.yml

@@ -22,6 +22,17 @@ llm:
   timeout-ms: 180000
   multimodal-enabled: true
   send-video-urls: true
+  omni:
+    enabled: true
+    provider: qwen
+    base-url: "https://llm-ax2z3lqqt75hg7cu.cn-beijing.maas.aliyuncs.com/compatible-mode/v1"
+    endpoint: /chat/completions
+    api-key: "sk-ws-H.ERXPIHX.ITCw.MEYCIQD9fnRpudQRlLLQKL2faPTJejqa13VV5t2Q7pRGF-MV7wIhANeQ-QRx4eoJeTxy1QtV8B5lemcIzyr05s3fTMH5ARUO"
+    model: qwen3.5-omni-plus
+    auth-header: Authorization
+    auth-prefix: Bearer
+    timeout-ms: 180000
+    send-video-urls: true
 
 asr:
   enabled: true

+ 11 - 0
src/main/resources/application.yml

@@ -32,6 +32,17 @@ llm:
   timeout-ms: 180000
   multimodal-enabled: true
   send-video-urls: true
+  omni:
+    enabled: false
+    provider: qwen
+    base-url: ""
+    endpoint: /chat/completions
+    api-key: ""
+    model: qwen3.5-omni-plus
+    auth-header: Authorization
+    auth-prefix: Bearer
+    timeout-ms: 180000
+    send-video-urls: true
 
 asr:
   enabled: false

+ 15 - 12
src/main/resources/mapper/VideoScriptTaskMapper.xml

@@ -5,8 +5,8 @@
 <mapper namespace="com.video.script.admin.mapper.VideoScriptTaskMapper">
 
     <select id="selectList" resultType="com.video.script.admin.entity.VideoScriptTask">
-        select t.id, t.task_name, t.video_url, t.video_urls, t.script_type, t.status, t.prompt_hint, t.transcript, t.video_content, t.analysis_content, t.script_content,
-               t.summary, t.codedesk_task_id, t.error_message, t.creator_id, u.nickname as creator_name,
+        select t.id, t.task_name, t.video_url, t.video_urls, t.script_type, t.status, t.prompt_hint, t.transcript, t.video_content, t.omni_content, t.analysis_content, t.script_content,
+               t.summary, t.codedesk_task_id, t.use_omni_analysis, t.error_message, t.creator_id, u.nickname as creator_name,
                t.create_time, t.update_time
         from video_script_task t
         left join sys_user u on t.creator_id = u.id
@@ -22,33 +22,33 @@
     </select>
 
     <select id="selectById" resultType="com.video.script.admin.entity.VideoScriptTask">
-        select id, task_name, video_url, video_urls, script_type, status, prompt_hint, transcript, video_content, analysis_content, script_content, summary,
-               codedesk_task_id, error_message, creator_id, create_time, update_time
+        select id, task_name, video_url, video_urls, script_type, status, prompt_hint, transcript, video_content, omni_content, analysis_content, script_content, summary,
+               codedesk_task_id, use_omni_analysis, error_message, creator_id, create_time, update_time
         from video_script_task
         where id = #{id}
     </select>
 
     <select id="selectByCodedeskTaskId" resultType="com.video.script.admin.entity.VideoScriptTask">
-        select id, task_name, video_url, video_urls, script_type, status, prompt_hint, transcript, video_content, analysis_content, script_content, summary,
-               codedesk_task_id, error_message, creator_id, create_time, update_time
+        select id, task_name, video_url, video_urls, script_type, status, prompt_hint, transcript, video_content, omni_content, analysis_content, script_content, summary,
+               codedesk_task_id, use_omni_analysis, error_message, creator_id, create_time, update_time
         from video_script_task
         where codedesk_task_id = #{codedeskTaskId}
         limit 1
     </select>
 
     <select id="selectByStatus" resultType="com.video.script.admin.entity.VideoScriptTask">
-        select id, task_name, video_url, video_urls, script_type, status, prompt_hint, transcript, video_content, analysis_content, script_content, summary,
-               codedesk_task_id, error_message, creator_id, create_time, update_time
+        select id, task_name, video_url, video_urls, script_type, status, prompt_hint, transcript, video_content, omni_content, analysis_content, script_content, summary,
+               codedesk_task_id, use_omni_analysis, error_message, creator_id, create_time, update_time
         from video_script_task
         where status = #{status}
         order by id asc
     </select>
 
     <insert id="insert" useGeneratedKeys="true" keyProperty="id">
-        insert into video_script_task (task_name, video_url, video_urls, script_type, status, prompt_hint, transcript, video_content, analysis_content, script_content, summary,
-                                       codedesk_task_id, error_message, creator_id, create_time, update_time)
-        values (#{taskName}, #{videoUrl}, #{videoUrls}, #{scriptType}, #{status}, #{promptHint}, #{transcript}, #{videoContent}, #{analysisContent}, #{scriptContent}, #{summary},
-                #{codedeskTaskId}, #{errorMessage}, #{creatorId}, now(), now())
+        insert into video_script_task (task_name, video_url, video_urls, script_type, status, prompt_hint, transcript, video_content, omni_content, analysis_content, script_content, summary,
+                                       codedesk_task_id, use_omni_analysis, error_message, creator_id, create_time, update_time)
+        values (#{taskName}, #{videoUrl}, #{videoUrls}, #{scriptType}, #{status}, #{promptHint}, #{transcript}, #{videoContent}, #{omniContent}, #{analysisContent}, #{scriptContent}, #{summary},
+                #{codedeskTaskId}, #{useOmniAnalysis}, #{errorMessage}, #{creatorId}, now(), now())
     </insert>
 
     <update id="update">
@@ -61,10 +61,12 @@
             prompt_hint = #{promptHint},
             transcript = #{transcript},
             video_content = #{videoContent},
+            omni_content = #{omniContent},
             analysis_content = #{analysisContent},
             script_content = #{scriptContent},
             summary = #{summary},
             codedesk_task_id = #{codedeskTaskId},
+            use_omni_analysis = #{useOmniAnalysis},
             error_message = #{errorMessage},
             update_time = now()
         where id = #{id}
@@ -83,6 +85,7 @@
         set status = #{status},
             transcript = #{transcript},
             video_content = #{videoContent},
+            omni_content = #{omniContent},
             analysis_content = #{analysisContent},
             script_content = #{scriptContent},
             summary = #{summary},

+ 2 - 0
src/main/resources/sql/schema.sql

@@ -65,10 +65,12 @@ create table if not exists video_script_task (
     prompt_hint text,
     transcript text,
     video_content text,
+    omni_content text,
     analysis_content text,
     script_content text,
     summary text,
     codedesk_task_id varchar(128) default null,
+    use_omni_analysis tinyint(1) not null default 0,
     error_message text,
     creator_id bigint not null,
     create_time datetime not null,

+ 6 - 0
src/main/resources/sql/upgrade_video_script_task.sql

@@ -6,3 +6,9 @@ alter table video_script_task
 
 alter table video_script_task
     add column if not exists analysis_content text null after video_content;
+
+alter table video_script_task
+    add column if not exists omni_content text null after video_content;
+
+alter table video_script_task
+    add column if not exists use_omni_analysis tinyint(1) not null default 0 after codedesk_task_id;