feat: 实现分镜视频功能和提示词优化功能

主要功能:
1. 分镜视频创作功能
   - 支持文生图生成分镜图
   - 支持直接上传分镜图生成视频
   - 两步式流程:生成分镜图 -> 生成视频
   - 完整的任务管理和状态轮询

2. 提示词优化功能
   - 为所有创作页面添加一键优化按钮
   - 支持三种优化类型:文生视频、图生视频、分镜视频
   - 使用GPT-4o-mini进行智能优化
   - 完善的错误处理和用户体验

技术改进:
- 使用@Async和@Transactional优化异步处理
- 增强错误处理和超时控制
- 改进前端状态管理和用户体验
- 添加完整的代码审查文档
This commit is contained in:
AIGC Developer
2025-10-29 18:25:26 +08:00
parent 6f72386523
commit 7964d87954
18 changed files with 1939 additions and 102 deletions

View File

@@ -0,0 +1,107 @@
package com.example.demo.controller;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.example.demo.service.RealAIService;
@RestController
@RequestMapping("/api/prompt")
public class PromptOptimizerApiController {
private static final Logger logger = LoggerFactory.getLogger(PromptOptimizerApiController.class);
@Autowired
private RealAIService realAIService;
/**
* 优化提示词
*
* @param request 包含prompt和type的请求体
* @param authentication 用户认证信息
* @return 优化后的提示词
*/
@PostMapping("/optimize")
public ResponseEntity<?> optimizePrompt(
@RequestBody Map<String, Object> request,
Authentication authentication) {
try {
String username = authentication.getName();
logger.info("收到优化提示词请求,用户: {}", username);
// 从请求中提取参数
String prompt = (String) request.get("prompt");
String type = (String) request.getOrDefault("type", "text-to-video");
// 参数验证
if (prompt == null || prompt.trim().isEmpty()) {
return ResponseEntity.badRequest()
.body(Map.of("success", false, "message", "提示词不能为空"));
}
// 长度验证(防止过长)
if (prompt.length() > 2000) {
return ResponseEntity.badRequest()
.body(Map.of("success", false, "message", "提示词过长请控制在2000字符以内"));
}
// 验证type是否有效
if (!isValidType(type)) {
logger.warn("无效的优化类型: {}, 使用默认类型: text-to-video", type);
type = "text-to-video"; // 默认类型
}
// 调用优化服务
String optimizedPrompt = realAIService.optimizePrompt(prompt.trim(), type);
// 检查优化是否成功(如果返回原始提示词可能是失败)
boolean optimized = !optimizedPrompt.equals(prompt.trim());
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", optimized ? "提示词优化成功" : "提示词优化完成(可能使用了原始提示词)");
response.put("data", Map.of(
"originalPrompt", prompt,
"optimizedPrompt", optimizedPrompt,
"type", type,
"optimized", optimized
));
logger.info("提示词优化完成,用户: {}, 类型: {}, 原始长度: {}, 优化后长度: {}",
username, type, prompt.length(), optimizedPrompt.length());
return ResponseEntity.ok(response);
} catch (IllegalArgumentException e) {
logger.error("参数错误: {}", e.getMessage());
return ResponseEntity.badRequest()
.body(Map.of("success", false, "message", "参数错误: " + e.getMessage()));
} catch (Exception e) {
logger.error("优化提示词失败", e);
return ResponseEntity.internalServerError()
.body(Map.of("success", false, "message", "优化提示词失败,请稍后重试"));
}
}
/**
* 验证类型是否有效
*/
private boolean isValidType(String type) {
return type != null && (
type.equals("text-to-video") ||
type.equals("image-to-video") ||
type.equals("storyboard")
);
}
}

View File

@@ -0,0 +1,151 @@
package com.example.demo.controller;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.example.demo.model.StoryboardVideoTask;
import com.example.demo.service.StoryboardVideoService;
@RestController
@RequestMapping("/api/storyboard-video")
public class StoryboardVideoApiController {
private static final Logger logger = LoggerFactory.getLogger(StoryboardVideoApiController.class);
@Autowired
private StoryboardVideoService storyboardVideoService;
/**
* 创建分镜视频任务
*/
@PostMapping("/create")
public ResponseEntity<?> createTask(
@RequestBody Map<String, Object> request,
Authentication authentication) {
try {
String username = authentication.getName();
logger.info("收到创建分镜视频任务请求,用户: {}", username);
// 从请求中提取参数
String prompt = (String) request.get("prompt");
String aspectRatio = (String) request.getOrDefault("aspectRatio", "16:9");
Boolean hdMode = (Boolean) request.getOrDefault("hdMode", false);
String imageUrl = (String) request.get("imageUrl");
if (prompt == null || prompt.trim().isEmpty()) {
return ResponseEntity.badRequest()
.body(Map.of("success", false, "message", "提示词不能为空"));
}
// 创建任务
StoryboardVideoTask task = storyboardVideoService.createTask(
username, prompt, aspectRatio, hdMode != null && hdMode, imageUrl
);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "任务创建成功");
response.put("data", Map.of(
"taskId", task.getTaskId(),
"status", task.getStatus(),
"progress", task.getProgress()
));
return ResponseEntity.ok(response);
} catch (IllegalArgumentException e) {
logger.error("参数错误: {}", e.getMessage(), e);
return ResponseEntity.badRequest()
.body(Map.of("success", false, "message", e.getMessage()));
} catch (Exception e) {
logger.error("创建分镜视频任务失败", e);
e.printStackTrace(); // 打印完整堆栈
return ResponseEntity.internalServerError()
.body(Map.of("success", false, "message", "创建任务失败: " + e.getMessage()));
}
}
/**
* 获取任务详情
*/
@GetMapping("/task/{taskId}")
public ResponseEntity<?> getTask(@PathVariable String taskId, Authentication authentication) {
try {
String username = authentication.getName();
logger.info("收到获取分镜视频任务详情请求任务ID: {}, 用户: {}", taskId, username);
StoryboardVideoTask task = storyboardVideoService.getTask(taskId);
// 验证用户权限
if (!task.getUsername().equals(username)) {
logger.warn("用户 {} 尝试访问任务 {},但任务属于用户 {}", username, taskId, task.getUsername());
return ResponseEntity.status(403)
.body(Map.of("success", false, "message", "无权访问此任务"));
}
Map<String, Object> taskData = new HashMap<>();
taskData.put("taskId", task.getTaskId());
taskData.put("status", task.getStatus());
taskData.put("progress", task.getProgress());
taskData.put("resultUrl", task.getResultUrl());
taskData.put("errorMessage", task.getErrorMessage());
taskData.put("createdAt", task.getCreatedAt());
taskData.put("updatedAt", task.getUpdatedAt());
taskData.put("completedAt", task.getCompletedAt());
return ResponseEntity.ok(Map.of(
"success", true,
"data", taskData
));
} catch (RuntimeException e) {
logger.error("获取任务详情失败: {}", e.getMessage());
return ResponseEntity.status(404)
.body(Map.of("success", false, "message", "任务不存在"));
} catch (Exception e) {
logger.error("获取任务详情异常", e);
return ResponseEntity.internalServerError()
.body(Map.of("success", false, "message", "查询失败"));
}
}
/**
* 获取用户任务列表
*/
@GetMapping("/tasks")
public ResponseEntity<?> getUserTasks(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
Authentication authentication) {
try {
String username = authentication.getName();
List<StoryboardVideoTask> tasks = storyboardVideoService.getUserTasks(username, page, size);
return ResponseEntity.ok(Map.of(
"success", true,
"data", tasks,
"page", page,
"size", size
));
} catch (Exception e) {
logger.error("获取用户任务列表失败", e);
return ResponseEntity.internalServerError()
.body(Map.of("success", false, "message", "查询失败"));
}
}
}

View File

@@ -0,0 +1,156 @@
package com.example.demo.model;
import java.time.LocalDateTime;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
/**
* 分镜视频任务实体
*/
@Entity
@Table(name = "storyboard_video_tasks")
public class StoryboardVideoTask {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 50)
private String taskId;
@Column(nullable = false, length = 100)
private String username;
@Column(columnDefinition = "TEXT")
private String prompt; // 文本描述
@Column(length = 500)
private String imageUrl; // 上传的参考图片URL可选
@Column(nullable = false, length = 10)
private String aspectRatio; // 16:9, 4:3, 1:1, 3:4, 9:16
@Column(nullable = false)
private boolean hdMode; // 是否高清模式
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private TaskStatus status;
@Column(nullable = false)
private int progress; // 0-100
@Column(length = 500)
private String resultUrl; // 分镜图URL
@Column(name = "real_task_id")
private String realTaskId;
@Column(columnDefinition = "TEXT")
private String errorMessage;
@Column(nullable = false)
private int costPoints; // 消耗积分
@Column(nullable = false, updatable = false)
private LocalDateTime createdAt;
@Column(nullable = false)
private LocalDateTime updatedAt;
@Column
private LocalDateTime completedAt;
public enum TaskStatus {
PENDING, PROCESSING, COMPLETED, FAILED, CANCELLED
}
// 构造函数
public StoryboardVideoTask() {
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
public StoryboardVideoTask(String username, String prompt, String aspectRatio, boolean hdMode) {
this();
this.username = username;
this.prompt = prompt;
this.aspectRatio = aspectRatio;
this.hdMode = hdMode;
// 计算消耗积分
this.costPoints = calculateCost();
}
/**
* 计算任务消耗积分
*/
private Integer calculateCost() {
int baseCost = 10; // 基础消耗
int hdCost = hdMode ? 20 : 0; // 高清模式消耗
return baseCost + hdCost;
}
/**
* 更新任务状态
*/
public void updateStatus(TaskStatus newStatus) {
this.status = newStatus;
this.updatedAt = LocalDateTime.now();
// 任务结束状态都应该设置完成时间
if (newStatus == TaskStatus.COMPLETED || newStatus == TaskStatus.FAILED || newStatus == TaskStatus.CANCELLED) {
this.completedAt = LocalDateTime.now();
}
}
/**
* 更新进度
*/
public void updateProgress(Integer progress) {
this.progress = Math.min(100, Math.max(0, progress));
this.updatedAt = LocalDateTime.now();
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getTaskId() { return taskId; }
public void setTaskId(String taskId) { this.taskId = taskId; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPrompt() { return prompt; }
public void setPrompt(String prompt) { this.prompt = prompt; }
public String getImageUrl() { return imageUrl; }
public void setImageUrl(String imageUrl) { this.imageUrl = imageUrl; }
public String getAspectRatio() { return aspectRatio; }
public void setAspectRatio(String aspectRatio) { this.aspectRatio = aspectRatio; }
public boolean isHdMode() { return hdMode; }
public void setHdMode(boolean hdMode) { this.hdMode = hdMode; }
public TaskStatus getStatus() { return status; }
public void setStatus(TaskStatus status) { this.status = status; }
public int getProgress() { return progress; }
public void setProgress(int progress) { this.progress = progress; }
public String getResultUrl() { return resultUrl; }
public void setResultUrl(String resultUrl) { this.resultUrl = resultUrl; }
public String getErrorMessage() { return errorMessage; }
public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; }
public String getRealTaskId() { return realTaskId; }
public void setRealTaskId(String realTaskId) { this.realTaskId = realTaskId; }
public int getCostPoints() { return costPoints; }
public void setCostPoints(int costPoints) { this.costPoints = costPoints; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
public LocalDateTime getCompletedAt() { return completedAt; }
public void setCompletedAt(LocalDateTime completedAt) { this.completedAt = completedAt; }
}

View File

@@ -0,0 +1,23 @@
package com.example.demo.repository;
import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.example.demo.model.StoryboardVideoTask;
@Repository
public interface StoryboardVideoTaskRepository extends JpaRepository<StoryboardVideoTask, Long> {
Optional<StoryboardVideoTask> findByTaskId(String taskId);
List<StoryboardVideoTask> findByUsernameOrderByCreatedAtDesc(String username);
Page<StoryboardVideoTask> findByUsernameOrderByCreatedAtDesc(String username, Pageable pageable);
List<StoryboardVideoTask> findByStatus(StoryboardVideoTask.TaskStatus status);
}

View File

@@ -1,6 +1,7 @@
package com.example.demo.service;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -30,6 +31,12 @@ public class RealAIService {
@Value("${ai.api.key:ak_5f13ec469e6047d5b8155c3cc91350e2}")
private String aiApiKey;
@Value("${ai.image.api.base-url:https://ai.comfly.chat}")
private String aiImageApiBaseUrl;
@Value("${ai.image.api.key:ak_5f13ec469e6047d5b8155c3cc91350e2}")
private String aiImageApiKey;
private final ObjectMapper objectMapper;
@@ -379,6 +386,83 @@ public class RealAIService {
}
}
/**
* 提交文生图任务(分镜视频使用)
* 调用Qwen文生图API
*/
public Map<String, Object> submitTextToImageTask(String prompt, String aspectRatio) {
try {
logger.info("提交文生图任务: prompt={}, aspectRatio={}", prompt, aspectRatio);
// 根据aspectRatio转换尺寸
String size = convertAspectRatioToImageSize(aspectRatio);
// 使用文生图的API端点Comfly API
String url = aiImageApiBaseUrl + "/v1/images/generations";
// 构建请求体
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("prompt", prompt);
requestBody.put("size", size);
requestBody.put("model", "qwen-image");
requestBody.put("n", 1);
requestBody.put("response_format", "url");
String requestBodyJson = objectMapper.writeValueAsString(requestBody);
logger.info("文生图请求URL: {}", url);
logger.info("文生图请求体: {}", requestBodyJson);
logger.info("使用的API密钥: {}", aiImageApiKey.substring(0, Math.min(10, aiImageApiKey.length())) + "...");
HttpResponse<String> response = Unirest.post(url)
.header("Authorization", "Bearer " + aiImageApiKey)
.header("Content-Type", "application/json")
.body(requestBodyJson)
.asString();
logger.info("文生图API响应状态: {}", response.getStatus());
logger.info("文生图API响应内容: {}", response.getBody());
if (response.getStatus() == 200 && response.getBody() != null) {
@SuppressWarnings("unchecked")
Map<String, Object> responseBody = objectMapper.readValue(response.getBody(), Map.class);
// 检查是否有data字段
if (responseBody.get("data") != null) {
logger.info("文生图任务提交成功: {}", responseBody);
return responseBody;
} else {
logger.error("文生图任务提交失败: {}", responseBody);
throw new RuntimeException("任务提交失败: " + responseBody.get("message"));
}
} else {
logger.error("文生图任务提交失败HTTP状态: {}", response.getStatus());
throw new RuntimeException("任务提交失败HTTP状态: " + response.getStatus());
}
} catch (UnirestException e) {
logger.error("提交文生图任务异常", e);
throw new RuntimeException("提交任务失败: " + e.getMessage());
} catch (Exception e) {
logger.error("提交文生图任务异常", e);
throw new RuntimeException("提交任务失败: " + e.getMessage());
}
}
/**
* 将宽高比转换为图片尺寸
*/
private String convertAspectRatioToImageSize(String aspectRatio) {
return switch (aspectRatio) {
case "16:9" -> "1024x576";
case "9:16" -> "576x1024";
case "4:3" -> "1024x768";
case "3:4" -> "768x1024";
case "1:1" -> "1024x1024";
default -> "1024x768";
};
}
/**
* 将宽高比转换为Sora2 API的size参数
*/
@@ -390,4 +474,203 @@ public class RealAIService {
default -> "720x1280"; // 默认竖屏
};
}
/**
* 优化提示词
* 使用AI模型将用户输入的简单描述优化为详细、专业的提示词
*
* @param prompt 原始提示词
* @param type 优化类型text-to-video, image-to-video, storyboard
* @return 优化后的提示词,失败时返回原始提示词
*/
public String optimizePrompt(String prompt, String type) {
// 参数验证
if (prompt == null || prompt.trim().isEmpty()) {
logger.warn("提示词为空,无法优化");
return prompt;
}
// 长度限制(防止过长提示词)
if (prompt.length() > 1000) {
logger.warn("提示词过长({}字符截取前1000字符", prompt.length());
prompt = prompt.substring(0, 1000);
}
try {
logger.info("开始优化提示词: prompt={}, type={}, length={}",
prompt.length() > 50 ? prompt.substring(0, 50) + "..." : prompt,
type,
prompt.length());
// 根据类型生成不同的优化指令
String systemPrompt = getOptimizationPrompt(type);
// 构建请求体使用ChatGPT API格式
String url = aiApiBaseUrl + "/v1/chat/completions";
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("model", "gpt-4o-mini"); // 使用GPT-4o-mini进行优化
List<Map<String, String>> messages = new java.util.ArrayList<>();
Map<String, String> systemMessage = new HashMap<>();
systemMessage.put("role", "system");
systemMessage.put("content", systemPrompt);
messages.add(systemMessage);
Map<String, String> userMessage = new HashMap<>();
userMessage.put("role", "user");
userMessage.put("content", "请优化以下提示词,保持原始意图:\n" + prompt);
messages.add(userMessage);
requestBody.put("messages", messages);
requestBody.put("temperature", 0.7);
requestBody.put("max_tokens", 800); // 增加token限制允许更详细的优化
String requestBodyJson = objectMapper.writeValueAsString(requestBody);
logger.debug("提示词优化请求URL: {}", url);
// 设置超时时间30秒
HttpResponse<String> response = Unirest.post(url)
.header("Authorization", "Bearer " + aiApiKey)
.header("Content-Type", "application/json")
.socketTimeout(30000)
.connectTimeout(10000)
.body(requestBodyJson)
.asString();
int statusCode = response.getStatus();
logger.info("提示词优化API响应状态: {}", statusCode);
if (statusCode == 200 && response.getBody() != null) {
try {
@SuppressWarnings("unchecked")
Map<String, Object> responseBody = objectMapper.readValue(response.getBody(), Map.class);
// 检查API错误响应
if (responseBody.containsKey("error")) {
@SuppressWarnings("unchecked")
Map<String, Object> error = (Map<String, Object>) responseBody.get("error");
String errorMessage = (String) error.get("message");
logger.error("API返回错误: {}", errorMessage);
return prompt; // 返回原始提示词
}
@SuppressWarnings("unchecked")
List<Map<String, Object>> choices = (List<Map<String, Object>>) responseBody.get("choices");
if (choices != null && !choices.isEmpty()) {
@SuppressWarnings("unchecked")
Map<String, Object> choice = choices.get(0);
// 检查finish_reason
String finishReason = (String) choice.get("finish_reason");
if ("length".equals(finishReason)) {
logger.warn("优化结果被截断可能需要增加max_tokens");
}
@SuppressWarnings("unchecked")
Map<String, Object> message = (Map<String, Object>) choice.get("message");
if (message != null) {
String optimizedPrompt = (String) message.get("content");
if (optimizedPrompt != null && !optimizedPrompt.trim().isEmpty()) {
optimizedPrompt = optimizedPrompt.trim();
// 移除可能的前后缀标记
optimizedPrompt = optimizedPrompt.replaceAll("^\"+|\"+$", "");
logger.info("提示词优化成功: 原始长度={}, 优化后长度={}",
prompt.length(), optimizedPrompt.length());
return optimizedPrompt;
}
}
}
logger.warn("API响应格式异常无法解析优化结果");
} catch (Exception parseException) {
logger.error("解析API响应失败", parseException);
}
} else {
logger.error("提示词优化API请求失败HTTP状态: {}, 响应: {}",
statusCode,
response.getBody() != null && response.getBody().length() < 200 ? response.getBody() : "响应过长");
}
// 所有失败情况都返回原始提示词
return prompt;
} catch (com.fasterxml.jackson.core.JsonProcessingException e) {
logger.error("提示词优化JSON处理失败", e);
return prompt;
} catch (kong.unirest.UnirestException e) {
// Unirest异常可能包含超时或连接错误
Throwable cause = e.getCause();
if (cause instanceof java.net.SocketTimeoutException) {
logger.error("提示词优化超时", e);
} else if (cause instanceof java.net.ConnectException) {
logger.error("提示词优化连接失败", e);
} else {
logger.error("提示词优化网络异常", e);
}
return prompt;
} catch (Exception e) {
logger.error("提示词优化异常", e);
return prompt;
}
}
/**
* 根据类型获取优化提示词的系统指令
* 使用文本块Text Blocks优化可读性
*/
private String getOptimizationPrompt(String type) {
return switch (type) {
case "text-to-video" -> """
你是一个专业的视频生成提示词优化专家。你的任务是将用户提供的简单描述优化为详细、专业、适合AI视频生成的英文提示词。
优化要求:
1. 将中文描述翻译成流畅的英文,确保语义准确
2. 添加详细的视觉细节描述(场景环境、人物特征、动作细节、光线效果、色彩风格、构图方式等)
3. 使用专业的电影术语和视觉词汇cinematic, wide shot, close-up等
4. 确保提示词清晰、具体、有画面感能够准确指导AI生成
5. 保持原始意图不变,只优化表达方式和补充细节
6. 如果原始提示词已经是英文,直接优化,保持语言一致
7. 输出优化后的提示词,不要添加额外说明、引号或其他格式标记
8. 优化后的提示词应该直接可用,长度控制在合理范围内
""";
case "image-to-video" -> """
你是一个专业的视频生成提示词优化专家。你的任务是将用户提供的简单描述优化为详细、专业、适合AI图生视频的英文提示词。
优化要求:
1. 将中文描述翻译成流畅的英文,确保语义准确
2. 重点关注动画效果、镜头运动、动作描述zoom in, pan, fade等
3. 添加时间维度和动态效果描述(持续时间、运动方向、速度变化等)
4. 确保提示词能准确表达图片到视频的转换需求,描述动画和运动的细节
5. 使用专业的视频制作术语smooth transition, dynamic movement等
6. 如果原始提示词已经是英文,直接优化,保持语言一致
7. 输出优化后的提示词,不要添加额外说明、引号或其他格式标记
8. 优化后的提示词应该直接可用,长度控制在合理范围内
""";
case "storyboard" -> """
你是一个专业的分镜图提示词优化专家。你的任务是将用户提供的简单描述优化为详细、专业、适合生成故事板分镜图的英文提示词。
优化要求:
1. 将中文描述翻译成流畅的英文,确保语义准确
2. 关注镜头构图、画面布局、视觉元素composition, framing, visual hierarchy等
3. 适合生成12格黑白分镜图风格强调构图和画面元素
4. 确保提示词清晰描述每个镜头的关键视觉元素和构图方式
5. 使用专业的电影分镜术语establishing shot, medium shot, close-up等
6. 如果原始提示词已经是英文,直接优化,保持语言一致
7. 输出优化后的提示词,不要添加额外说明、引号或其他格式标记
8. 优化后的提示词应该直接可用,长度控制在合理范围内
""";
default -> """
你是一个专业的提示词优化专家。请将用户提供的简单描述优化为详细、专业的英文提示词。
优化要求:
1. 将中文描述翻译成流畅的英文,如果已经是英文则直接优化
2. 添加必要的视觉细节和描述,使提示词更加具体和专业
3. 保持原始意图不变,只优化表达方式
4. 输出优化后的提示词,不要添加额外说明、引号或其他格式标记
5. 优化后的提示词应该直接可用,长度控制在合理范围内
""";
};
}
}

View File

@@ -0,0 +1,175 @@
package com.example.demo.service;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.example.demo.model.StoryboardVideoTask;
import com.example.demo.repository.StoryboardVideoTaskRepository;
/**
* 分镜视频服务类
*/
@Service
@Transactional
public class StoryboardVideoService {
private static final Logger logger = LoggerFactory.getLogger(StoryboardVideoService.class);
@Autowired
private StoryboardVideoTaskRepository taskRepository;
@Autowired
private RealAIService realAIService;
/**
* 创建分镜视频任务
*/
public StoryboardVideoTask createTask(String username, String prompt, String aspectRatio, boolean hdMode, String imageUrl) {
try {
// 验证参数
if (username == null || username.trim().isEmpty()) {
throw new IllegalArgumentException("用户名不能为空");
}
if (prompt == null || prompt.trim().isEmpty()) {
throw new IllegalArgumentException("文本描述不能为空");
}
// 生成任务ID
String taskId = generateTaskId();
// 创建任务
StoryboardVideoTask task = new StoryboardVideoTask(username, prompt.trim(), aspectRatio, hdMode);
task.setTaskId(taskId);
task.setStatus(StoryboardVideoTask.TaskStatus.PENDING);
task.setProgress(0);
if (imageUrl != null && !imageUrl.isEmpty()) {
task.setImageUrl(imageUrl);
}
// 保存任务
task = taskRepository.save(task);
logger.info("分镜视频任务创建成功: {}, 用户: {}", taskId, username);
// 异步处理任务
processTaskAsync(taskId);
return task;
} catch (Exception e) {
logger.error("创建分镜视频任务失败", e);
throw new RuntimeException("创建任务失败: " + e.getMessage());
}
}
/**
* 使用真实API处理任务异步
* 使用Spring的@Async注解自动管理事务边界
*/
@Async
@Transactional
public void processTaskAsync(String taskId) {
try {
logger.info("开始使用真实API处理分镜视频任务: {}", taskId);
// 重新从数据库加载任务,获取最新状态
StoryboardVideoTask task = taskRepository.findByTaskId(taskId)
.orElseThrow(() -> new RuntimeException("任务未找到: " + taskId));
// 更新任务状态为处理中
task.updateStatus(StoryboardVideoTask.TaskStatus.PROCESSING);
taskRepository.flush(); // 强制刷新到数据库
// 调用真实文生图API
logger.info("分镜视频任务已提交正在调用文生图API生成分镜图...");
Map<String, Object> apiResponse = realAIService.submitTextToImageTask(
task.getPrompt(),
task.getAspectRatio()
);
// 从API响应中提取图片URL
@SuppressWarnings("unchecked")
List<Map<String, Object>> data = (List<Map<String, Object>>) apiResponse.get("data");
if (data != null && !data.isEmpty()) {
String imageUrl = null;
Map<String, Object> firstImage = data.get(0);
// 检查是否有url或b64_json字段
if (firstImage.get("url") != null) {
imageUrl = (String) firstImage.get("url");
} else if (firstImage.get("b64_json") != null) {
// base64编码的图片
String base64Data = (String) firstImage.get("b64_json");
imageUrl = "data:image/png;base64," + base64Data;
}
// 重新加载任务因为之前的flush可能使实体detached
task = taskRepository.findByTaskId(taskId)
.orElseThrow(() -> new RuntimeException("任务未找到: " + taskId));
// 设置结果
task.setResultUrl(imageUrl);
task.setRealTaskId(taskId + "_image");
task.updateStatus(StoryboardVideoTask.TaskStatus.COMPLETED);
task.updateProgress(100);
taskRepository.save(task);
logger.info("分镜图生成完成任务ID: {}, 图片URL: {}", taskId, imageUrl);
} else {
throw new RuntimeException("API返回的图片数据为空");
}
} catch (Exception e) {
logger.error("处理分镜视频任务失败: {}", taskId, e);
try {
StoryboardVideoTask task = taskRepository.findByTaskId(taskId)
.orElseThrow(() -> new RuntimeException("任务未找到: " + taskId));
task.updateStatus(StoryboardVideoTask.TaskStatus.FAILED);
task.setErrorMessage(e.getMessage());
taskRepository.save(task);
} catch (Exception ex) {
logger.error("更新任务失败状态失败: {}", taskId, ex);
}
}
}
/**
* 获取任务详情
*/
@Transactional(readOnly = true)
public StoryboardVideoTask getTask(String taskId) {
return taskRepository.findByTaskId(taskId)
.orElseThrow(() -> new RuntimeException("任务不存在: " + taskId));
}
/**
* 获取用户任务列表
*/
@Transactional(readOnly = true)
public List<StoryboardVideoTask> getUserTasks(String username, int page, int size) {
Pageable pageable = PageRequest.of(page, size);
Page<StoryboardVideoTask> taskPage = taskRepository.findByUsernameOrderByCreatedAtDesc(username, pageable);
return taskPage.getContent();
}
/**
* 生成任务ID
*/
private String generateTaskId() {
return "sb_" + UUID.randomUUID().toString().replace("-", "").substring(0, 16);
}
}

View File

@@ -28,6 +28,14 @@ tencent.ses.region=ap-beijing
tencent.ses.from-email=noreply@vionow.com
tencent.ses.from-name=AIGC平台
# AI API配置
# 文生视频、图生视频、分镜视频都使用Comfly API
ai.api.base-url=https://ai.comfly.chat
ai.api.key=sk-jp1O4h5lfN3WWZReF48SDa2osm0o9alC9qetkgq3M7XUjJ4R
# 文生图使用Comfly API (在代码中单独配置)
ai.image.api.base-url=https://ai.comfly.chat
ai.image.api.key=sk-jp1O4h5lfN3WWZReF48SDa2osm0o9alC9qetkgq3M7XUjJ4R
# 支付宝配置 (开发环境 - 沙箱测试)
# 请替换为您的实际配置
alipay.app-id=9021000157616562

View File

@@ -0,0 +1,22 @@
-- 创建分镜视频任务表
CREATE TABLE IF NOT EXISTS storyboard_video_tasks (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
task_id VARCHAR(50) NOT NULL UNIQUE,
username VARCHAR(100) NOT NULL,
prompt TEXT,
image_url VARCHAR(500),
aspect_ratio VARCHAR(10) NOT NULL,
hd_mode BOOLEAN NOT NULL DEFAULT FALSE,
status VARCHAR(20) NOT NULL,
progress INT NOT NULL DEFAULT 0,
result_url VARCHAR(500),
real_task_id VARCHAR(255),
error_message TEXT,
cost_points INT NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
completed_at TIMESTAMP,
INDEX idx_username (username),
INDEX idx_status (status),
INDEX idx_task_id (task_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;