Commit 787ae772 authored by yaoke.yk's avatar yaoke.yk

客户端加入首未帧和道具修改

parent 232de8ef
...@@ -340,6 +340,15 @@ export function useUpdateStoryboard(projectId: string, episodeId: string) { ...@@ -340,6 +340,15 @@ export function useUpdateStoryboard(projectId: string, episodeId: string) {
}); });
} }
export function useUploadStoryboardFrameImage(projectId: string, episodeId: string) {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ id, frameType, file }: { id: string; frameType: "first" | "last"; file: File }) =>
aiApi.uploadStoryboardFrameImage(projectId, id, frameType, file),
onSuccess: () => qc.invalidateQueries({ queryKey: storyboardsKey(projectId, episodeId) }),
});
}
export function useDeleteStoryboard(projectId: string, episodeId: string) { export function useDeleteStoryboard(projectId: string, episodeId: string) {
const qc = useQueryClient(); const qc = useQueryClient();
return useMutation({ return useMutation({
......
...@@ -47,6 +47,10 @@ export interface Storyboard { ...@@ -47,6 +47,10 @@ export interface Storyboard {
sceneImageKey?: string | null; sceneImageKey?: string | null;
propImageKeys?: string[] | null; propImageKeys?: string[] | null;
styleImageKey?: string | null; styleImageKey?: string | null;
firstFrameImageKey?: string | null;
firstFrameImageUrl?: string | null;
lastFrameImageKey?: string | null;
lastFrameImageUrl?: string | null;
} }
export interface ShotAsset { export interface ShotAsset {
...@@ -157,6 +161,8 @@ export interface StructuredVideoRequest { ...@@ -157,6 +161,8 @@ export interface StructuredVideoRequest {
sceneImageKey?: string | null; sceneImageKey?: string | null;
propImageKeys?: string[] | null; propImageKeys?: string[] | null;
styleImageKey?: string | null; styleImageKey?: string | null;
firstFrameImageKey?: string | null;
lastFrameImageKey?: string | null;
/** V17:单段 freeform 提示词(与 userPrompt 二选一,优先使用) */ /** V17:单段 freeform 提示词(与 userPrompt 二选一,优先使用) */
freeformPrompt?: string | null; freeformPrompt?: string | null;
userPrompt?: { userPrompt?: {
...@@ -234,6 +240,21 @@ export const aiApi = { ...@@ -234,6 +240,21 @@ export const aiApi = {
const r = await apiClient.put(`/projects/${projectId}/storyboards/${storyboardId}`, patch); const r = await apiClient.put(`/projects/${projectId}/storyboards/${storyboardId}`, patch);
return r.data.data; return r.data.data;
}, },
uploadStoryboardFrameImage: async (
projectId: string,
storyboardId: string,
frameType: "first" | "last",
file: File
): Promise<Storyboard> => {
const form = new FormData();
form.append("file", file);
const r = await apiClient.post(
`/projects/${projectId}/storyboards/${storyboardId}/frame-image?frameType=${frameType}`,
form,
{ headers: { "Content-Type": "multipart/form-data" }, timeout: 600_000 }
);
return r.data.data;
},
deleteStoryboard: async (projectId: string, storyboardId: string): Promise<void> => { deleteStoryboard: async (projectId: string, storyboardId: string): Promise<void> => {
await apiClient.delete(`/projects/${projectId}/storyboards/${storyboardId}`); await apiClient.delete(`/projects/${projectId}/storyboards/${storyboardId}`);
}, },
......
...@@ -8,7 +8,7 @@ import java.util.List; ...@@ -8,7 +8,7 @@ import java.util.List;
* *
* 图片顺序固定(即 @图N 标号): * 图片顺序固定(即 @图N 标号):
* @图1..@图K = 角色(K = 角色数量),@图K+1 = 场景, * @图1..@图K = 角色(K = 角色数量),@图K+1 = 场景,
* @图K+2.. = 道具(按顺序),最后一张 = 风格图(如有) * @图K+2.. = 道具(按顺序),之后是首/尾帧参考图,最后一张 = 风格图(如有)
*/ */
public final class SeedancePromptBuilder { public final class SeedancePromptBuilder {
...@@ -24,8 +24,14 @@ public final class SeedancePromptBuilder { ...@@ -24,8 +24,14 @@ public final class SeedancePromptBuilder {
/** V17:单段 freeform 提示词模式(前端单输入框后的主要入口)。 */ /** V17:单段 freeform 提示词模式(前端单输入框后的主要入口)。 */
public static String buildPromptFreeform(String freeformBody, int characterCount, public static String buildPromptFreeform(String freeformBody, int characterCount,
boolean hasSceneImage, int propCount, boolean hasStyleImage) { boolean hasSceneImage, int propCount, boolean hasStyleImage) {
return buildPromptFreeform(freeformBody, characterCount, hasSceneImage, propCount, 0, hasStyleImage);
}
public static String buildPromptFreeform(String freeformBody, int characterCount,
boolean hasSceneImage, int propCount, int frameCount,
boolean hasStyleImage) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
appendImageHeader(sb, characterCount, hasSceneImage, propCount, hasStyleImage); appendImageHeader(sb, characterCount, hasSceneImage, propCount, frameCount, hasStyleImage);
sb.append("\n分镜内容:\n"); sb.append("\n分镜内容:\n");
if (freeformBody != null && !freeformBody.isBlank()) { if (freeformBody != null && !freeformBody.isBlank()) {
sb.append(freeformBody.trim()).append('\n'); sb.append(freeformBody.trim()).append('\n');
...@@ -36,8 +42,14 @@ public final class SeedancePromptBuilder { ...@@ -36,8 +42,14 @@ public final class SeedancePromptBuilder {
public static String buildPrompt(UserPromptParts parts, int characterCount, public static String buildPrompt(UserPromptParts parts, int characterCount,
boolean hasSceneImage, int propCount, boolean hasStyleImage) { boolean hasSceneImage, int propCount, boolean hasStyleImage) {
return buildPrompt(parts, characterCount, hasSceneImage, propCount, 0, hasStyleImage);
}
public static String buildPrompt(UserPromptParts parts, int characterCount,
boolean hasSceneImage, int propCount, int frameCount,
boolean hasStyleImage) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
appendImageHeader(sb, characterCount, hasSceneImage, propCount, hasStyleImage); appendImageHeader(sb, characterCount, hasSceneImage, propCount, frameCount, hasStyleImage);
sb.append("\n分镜内容:\n"); sb.append("\n分镜内容:\n");
appendIfPresent(sb, "人物动作", parts.characterAction()); appendIfPresent(sb, "人物动作", parts.characterAction());
...@@ -56,6 +68,12 @@ public final class SeedancePromptBuilder { ...@@ -56,6 +68,12 @@ public final class SeedancePromptBuilder {
*/ */
private static void appendImageHeader(StringBuilder sb, int characterCount, private static void appendImageHeader(StringBuilder sb, int characterCount,
boolean hasSceneImage, int propCount, boolean hasStyleImage) { boolean hasSceneImage, int propCount, boolean hasStyleImage) {
appendImageHeader(sb, characterCount, hasSceneImage, propCount, 0, hasStyleImage);
}
private static void appendImageHeader(StringBuilder sb, int characterCount,
boolean hasSceneImage, int propCount, int frameCount,
boolean hasStyleImage) {
int idx = 0; int idx = 0;
if (characterCount == 1) { if (characterCount == 1) {
sb.append("@图").append(idx + 1) sb.append("@图").append(idx + 1)
...@@ -83,6 +101,16 @@ public final class SeedancePromptBuilder { ...@@ -83,6 +101,16 @@ public final class SeedancePromptBuilder {
} }
idx += propCount; idx += propCount;
} }
if (frameCount > 0) {
if (frameCount == 1) {
sb.append("@图").append(idx + 1)
.append(" 是分镜首帧或尾帧参考图,请作为画面起止状态参考,不要生硬复制成静态画面。\n");
} else {
sb.append("@图").append(idx + 1).append(" 是首帧参考图,@图").append(idx + 2)
.append(" 是尾帧参考图,请参考起始和结束画面状态,让视频动作自然衔接。\n");
}
idx += frameCount;
}
if (hasStyleImage) { if (hasStyleImage) {
sb.append("@图").append(idx + 1) sb.append("@图").append(idx + 1)
.append(" 是风格参考图,请参考整体美术风格、色彩和质感。\n"); .append(" 是风格参考图,请参考整体美术风格、色彩和质感。\n");
...@@ -111,6 +139,14 @@ public final class SeedancePromptBuilder { ...@@ -111,6 +139,14 @@ public final class SeedancePromptBuilder {
String sceneImageKey, String sceneImageKey,
List<String> propImageKeys, List<String> propImageKeys,
String styleImageKey) { String styleImageKey) {
return buildOrderedImageKeys(characterImageKeys, sceneImageKey, propImageKeys, null, styleImageKey);
}
public static List<String> buildOrderedImageKeys(List<String> characterImageKeys,
String sceneImageKey,
List<String> propImageKeys,
List<String> frameImageKeys,
String styleImageKey) {
List<String> keys = new ArrayList<>(); List<String> keys = new ArrayList<>();
if (characterImageKeys != null) { if (characterImageKeys != null) {
for (String k : characterImageKeys) { for (String k : characterImageKeys) {
...@@ -129,6 +165,13 @@ public final class SeedancePromptBuilder { ...@@ -129,6 +165,13 @@ public final class SeedancePromptBuilder {
} }
} }
} }
if (frameImageKeys != null) {
for (String k : frameImageKeys) {
if (k != null && !k.isBlank()) {
keys.add(k);
}
}
}
if (styleImageKey != null && !styleImageKey.isBlank()) { if (styleImageKey != null && !styleImageKey.isBlank()) {
keys.add(styleImageKey); keys.add(styleImageKey);
} }
......
...@@ -2,16 +2,23 @@ package com.yaoai.api.controller; ...@@ -2,16 +2,23 @@ package com.yaoai.api.controller;
import com.yaoai.api.dto.ai.StoryboardDTO; import com.yaoai.api.dto.ai.StoryboardDTO;
import com.yaoai.api.dto.ai.ShotAssetDTO; import com.yaoai.api.dto.ai.ShotAssetDTO;
import com.yaoai.common.exception.BizException;
import com.yaoai.common.exception.ErrorCode;
import com.yaoai.common.response.ApiResponse; import com.yaoai.common.response.ApiResponse;
import com.yaoai.domain.entity.Storyboard; import com.yaoai.domain.entity.Storyboard;
import com.yaoai.domain.mapper.StoryboardMapper;
import com.yaoai.pipeline.service.ShotAssetService; import com.yaoai.pipeline.service.ShotAssetService;
import com.yaoai.pipeline.service.StoryboardPipelineService; import com.yaoai.pipeline.service.StoryboardPipelineService;
import com.yaoai.security.context.TenantContext; import com.yaoai.security.context.TenantContext;
import com.yaoai.storage.service.TosService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
...@@ -24,6 +31,8 @@ public class StoryboardController { ...@@ -24,6 +31,8 @@ public class StoryboardController {
private final StoryboardPipelineService storyboardService; private final StoryboardPipelineService storyboardService;
private final ShotAssetService shotAssetService; private final ShotAssetService shotAssetService;
private final StoryboardMapper storyboardMapper;
private final TosService tosService;
@Operation(summary = "AI 批量生成分集分镜") @Operation(summary = "AI 批量生成分集分镜")
@PostMapping("/episodes/{episodeId}/storyboards/generate") @PostMapping("/episodes/{episodeId}/storyboards/generate")
...@@ -79,6 +88,47 @@ public class StoryboardController { ...@@ -79,6 +88,47 @@ public class StoryboardController {
return ApiResponse.success(StoryboardDTO.from(storyboardService.update(storyboardId, tenantId, req))); return ApiResponse.success(StoryboardDTO.from(storyboardService.update(storyboardId, tenantId, req)));
} }
@Operation(summary = "上传分镜首尾帧参考图")
@PostMapping(value = "/storyboards/{storyboardId}/frame-image", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ApiResponse<StoryboardDTO> uploadFrameImage(@PathVariable Long projectId,
@PathVariable Long storyboardId,
@RequestParam("frameType") String frameType,
@RequestPart("file") MultipartFile file) throws IOException {
Long tenantId = TenantContext.get();
Storyboard storyboard = storyboardMapper.selectById(storyboardId);
if (storyboard == null
|| !tenantId.equals(storyboard.getTenantId())
|| !projectId.equals(storyboard.getProjectId())) {
throw new BizException(ErrorCode.NOT_FOUND, "分镜不存在");
}
if (!"first".equals(frameType) && !"last".equals(frameType)) {
throw new BizException(ErrorCode.INVALID_PARAM, "frameType 仅支持 first / last");
}
if (file == null || file.isEmpty()) {
throw new BizException(ErrorCode.INVALID_PARAM, "图片文件不能为空");
}
String contentType = file.getContentType() != null ? file.getContentType() : "application/octet-stream";
if (!contentType.startsWith("image/")) {
throw new BizException(ErrorCode.INVALID_PARAM, "请上传图片文件");
}
String key = TosService.buildKey(tenantId, projectId, "storyboard-frames/" + frameType, file.getOriginalFilename());
tosService.upload(key, file.getInputStream(), file.getSize(), contentType);
String url = tosService.publicUrl(key);
Storyboard patch = new Storyboard();
patch.setId(storyboardId);
if ("first".equals(frameType)) {
patch.setFirstFrameImageKey(key);
patch.setFirstFrameImageUrl(url);
} else {
patch.setLastFrameImageKey(key);
patch.setLastFrameImageUrl(url);
}
storyboardMapper.updateById(patch);
return ApiResponse.success(StoryboardDTO.from(storyboardMapper.selectById(storyboardId)));
}
@Operation(summary = "删除分镜") @Operation(summary = "删除分镜")
@DeleteMapping("/storyboards/{storyboardId}") @DeleteMapping("/storyboards/{storyboardId}")
public ApiResponse<Void> delete(@PathVariable Long projectId, public ApiResponse<Void> delete(@PathVariable Long projectId,
......
...@@ -46,8 +46,10 @@ public class VideoTaskController { ...@@ -46,8 +46,10 @@ public class VideoTaskController {
|| (req.getCharacterImageKey() != null && !req.getCharacterImageKey().isBlank()); || (req.getCharacterImageKey() != null && !req.getCharacterImageKey().isBlank());
boolean hasScene = req.getSceneImageKey() != null && !req.getSceneImageKey().isBlank(); boolean hasScene = req.getSceneImageKey() != null && !req.getSceneImageKey().isBlank();
boolean hasProp = req.getPropImageKeys() != null && !req.getPropImageKeys().isEmpty(); boolean hasProp = req.getPropImageKeys() != null && !req.getPropImageKeys().isEmpty();
if (!hasCharacter && !hasScene && !hasProp) { boolean hasFrame = (req.getFirstFrameImageKey() != null && !req.getFirstFrameImageKey().isBlank())
throw new BizException(ErrorCode.INVALID_PARAM, "请至少选择一张参考图(角色/场景/道具任一)"); || (req.getLastFrameImageKey() != null && !req.getLastFrameImageKey().isBlank());
if (!hasCharacter && !hasScene && !hasProp && !hasFrame) {
throw new BizException(ErrorCode.INVALID_PARAM, "请至少选择一张参考图(角色/场景/道具/首尾帧任一)");
} }
Long tenantId = TenantContext.get(); Long tenantId = TenantContext.get();
...@@ -60,6 +62,8 @@ public class VideoTaskController { ...@@ -60,6 +62,8 @@ public class VideoTaskController {
.sceneImageKey(req.getSceneImageKey()) .sceneImageKey(req.getSceneImageKey())
.propImageKeys(req.getPropImageKeys()) .propImageKeys(req.getPropImageKeys())
.styleImageKey(req.getStyleImageKey()) .styleImageKey(req.getStyleImageKey())
.firstFrameImageKey(req.getFirstFrameImageKey())
.lastFrameImageKey(req.getLastFrameImageKey())
.characterAction(up != null ? up.getCharacterAction() : null) .characterAction(up != null ? up.getCharacterAction() : null)
.sceneEvent(up != null ? up.getSceneEvent() : null) .sceneEvent(up != null ? up.getSceneEvent() : null)
.cameraMovement(up != null ? up.getCameraMovement() : null) .cameraMovement(up != null ? up.getCameraMovement() : null)
......
...@@ -41,6 +41,10 @@ public class StoryboardDTO { ...@@ -41,6 +41,10 @@ public class StoryboardDTO {
private String sceneImageKey; private String sceneImageKey;
private List<String> propImageKeys; private List<String> propImageKeys;
private String styleImageKey; private String styleImageKey;
private String firstFrameImageKey;
private String firstFrameImageUrl;
private String lastFrameImageKey;
private String lastFrameImageUrl;
public static StoryboardDTO from(Storyboard s) { public static StoryboardDTO from(Storyboard s) {
StoryboardDTO dto = new StoryboardDTO(); StoryboardDTO dto = new StoryboardDTO();
...@@ -68,6 +72,10 @@ public class StoryboardDTO { ...@@ -68,6 +72,10 @@ public class StoryboardDTO {
dto.setSceneImageKey(s.getSceneImageKey()); dto.setSceneImageKey(s.getSceneImageKey());
dto.setPropImageKeys(parseList(s.getPropImageKeys(), "propImageKeys", s.getId())); dto.setPropImageKeys(parseList(s.getPropImageKeys(), "propImageKeys", s.getId()));
dto.setStyleImageKey(s.getStyleImageKey()); dto.setStyleImageKey(s.getStyleImageKey());
dto.setFirstFrameImageKey(s.getFirstFrameImageKey());
dto.setFirstFrameImageUrl(s.getFirstFrameImageUrl());
dto.setLastFrameImageKey(s.getLastFrameImageKey());
dto.setLastFrameImageUrl(s.getLastFrameImageUrl());
return dto; return dto;
} }
......
...@@ -46,6 +46,12 @@ public class StructuredVideoGenerateRequest { ...@@ -46,6 +46,12 @@ public class StructuredVideoGenerateRequest {
/** 风格图(可选,固定为最后一张) */ /** 风格图(可选,固定为最后一张) */
private String styleImageKey; private String styleImageKey;
/** 首帧参考图(可选,作为视频生成参考图参与,不强制模型首帧锁定) */
private String firstFrameImageKey;
/** 尾帧参考图(可选,作为视频生成参考图参与,不强制模型尾帧锁定) */
private String lastFrameImageKey;
/** 视频时长(秒) */ /** 视频时长(秒) */
@Min(value = 5, message = "视频时长最短 5 秒") @Min(value = 5, message = "视频时长最短 5 秒")
@Max(value = 15, message = "视频时长最长 15 秒") @Max(value = 15, message = "视频时长最长 15 秒")
......
-- V21 分镜首尾帧参考图
ALTER TABLE storyboards
ADD COLUMN first_frame_image_key VARCHAR(512) DEFAULT NULL
COMMENT '该分镜首帧参考图 TOS key',
ADD COLUMN first_frame_image_url VARCHAR(1024) DEFAULT NULL
COMMENT '该分镜首帧参考图公开 URL',
ADD COLUMN last_frame_image_key VARCHAR(512) DEFAULT NULL
COMMENT '该分镜尾帧参考图 TOS key',
ADD COLUMN last_frame_image_url VARCHAR(1024) DEFAULT NULL
COMMENT '该分镜尾帧参考图公开 URL';
...@@ -43,6 +43,10 @@ public class Storyboard { ...@@ -43,6 +43,10 @@ public class Storyboard {
private String sceneImageKey; private String sceneImageKey;
private String propImageKeys; private String propImageKeys;
private String styleImageKey; private String styleImageKey;
private String firstFrameImageKey;
private String firstFrameImageUrl;
private String lastFrameImageKey;
private String lastFrameImageUrl;
/** draft / review / approved / generating / generated / failed */ /** draft / review / approved / generating / generated / failed */
private String status; private String status;
......
...@@ -29,6 +29,10 @@ public class StructuredVideoSubmission { ...@@ -29,6 +29,10 @@ public class StructuredVideoSubmission {
private List<String> propImageKeys; private List<String> propImageKeys;
/** 风格图 TOS key(可空,永远是最后一张) */ /** 风格图 TOS key(可空,永远是最后一张) */
private String styleImageKey; private String styleImageKey;
/** 首帧参考图 TOS key(可空,作为额外参考图参与) */
private String firstFrameImageKey;
/** 尾帧参考图 TOS key(可空,作为额外参考图参与) */
private String lastFrameImageKey;
/** 用户原始结构化输入(5 段或单段 freeform),用于落库存证 */ /** 用户原始结构化输入(5 段或单段 freeform),用于落库存证 */
private Map<String, String> userPromptRaw; private Map<String, String> userPromptRaw;
......
...@@ -23,6 +23,7 @@ import lombok.RequiredArgsConstructor; ...@@ -23,6 +23,7 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
...@@ -188,18 +189,20 @@ public class VideoTaskPipelineServiceImpl implements VideoTaskPipelineService { ...@@ -188,18 +189,20 @@ public class VideoTaskPipelineServiceImpl implements VideoTaskPipelineService {
} }
boolean hasScene = s.getSceneImageKey() != null && !s.getSceneImageKey().isBlank(); boolean hasScene = s.getSceneImageKey() != null && !s.getSceneImageKey().isBlank();
int propCount = s.getPropImageKeys() != null ? s.getPropImageKeys().size() : 0; int propCount = s.getPropImageKeys() != null ? s.getPropImageKeys().size() : 0;
List<String> frameImageKeys = frameImageKeys(s);
int frameCount = frameImageKeys.size();
boolean hasStyle = s.getStyleImageKey() != null && !s.getStyleImageKey().isBlank(); boolean hasStyle = s.getStyleImageKey() != null && !s.getStyleImageKey().isBlank();
// 至少要有一张参考图(角色/场景/道具任一),否则前端不应允许提交 // 至少要有一张参考图(角色/场景/道具/首尾帧任一),否则前端不应允许提交
if (characterKeys.isEmpty() && !hasScene && propCount == 0) { if (characterKeys.isEmpty() && !hasScene && propCount == 0 && frameCount == 0) {
throw new BizException(ErrorCode.INVALID_PARAM, "至少需要一张参考图(角色/场景/道具任一)"); throw new BizException(ErrorCode.INVALID_PARAM, "至少需要一张参考图(角色/场景/道具/首尾帧任一)");
} }
// 1. 拼装 final prompt + 图片顺序 // 1. 拼装 final prompt + 图片顺序
String finalPrompt; String finalPrompt;
if (s.getFreeformPrompt() != null && !s.getFreeformPrompt().isBlank()) { if (s.getFreeformPrompt() != null && !s.getFreeformPrompt().isBlank()) {
finalPrompt = SeedancePromptBuilder.buildPromptFreeform( finalPrompt = SeedancePromptBuilder.buildPromptFreeform(
s.getFreeformPrompt(), characterKeys.size(), hasScene, propCount, hasStyle); s.getFreeformPrompt(), characterKeys.size(), hasScene, propCount, frameCount, hasStyle);
} else { } else {
finalPrompt = SeedancePromptBuilder.buildPrompt( finalPrompt = SeedancePromptBuilder.buildPrompt(
new SeedancePromptBuilder.UserPromptParts( new SeedancePromptBuilder.UserPromptParts(
...@@ -212,6 +215,7 @@ public class VideoTaskPipelineServiceImpl implements VideoTaskPipelineService { ...@@ -212,6 +215,7 @@ public class VideoTaskPipelineServiceImpl implements VideoTaskPipelineService {
characterKeys.size(), characterKeys.size(),
hasScene, hasScene,
propCount, propCount,
frameCount,
hasStyle hasStyle
); );
} }
...@@ -219,6 +223,7 @@ public class VideoTaskPipelineServiceImpl implements VideoTaskPipelineService { ...@@ -219,6 +223,7 @@ public class VideoTaskPipelineServiceImpl implements VideoTaskPipelineService {
characterKeys, characterKeys,
s.getSceneImageKey(), s.getSceneImageKey(),
s.getPropImageKeys(), s.getPropImageKeys(),
frameImageKeys,
s.getStyleImageKey() s.getStyleImageKey()
); );
...@@ -260,7 +265,8 @@ public class VideoTaskPipelineServiceImpl implements VideoTaskPipelineService { ...@@ -260,7 +265,8 @@ public class VideoTaskPipelineServiceImpl implements VideoTaskPipelineService {
java.util.Map.of( java.util.Map.of(
"durationSeconds", durationOrDefault(s.getDurationSeconds()), "durationSeconds", durationOrDefault(s.getDurationSeconds()),
"ratio", s.getRatio() != null ? s.getRatio() : "16:9", "ratio", s.getRatio() != null ? s.getRatio() : "16:9",
"generateAudio", Boolean.TRUE.equals(s.getGenerateAudio()) "generateAudio", Boolean.TRUE.equals(s.getGenerateAudio()),
"frameImageKeys", frameImageKeys
) )
); );
log.info("Structured video task created: id={}, characters={}, images={}, duration={}s, ratio={}", log.info("Structured video task created: id={}, characters={}, images={}, duration={}s, ratio={}",
...@@ -287,6 +293,8 @@ public class VideoTaskPipelineServiceImpl implements VideoTaskPipelineService { ...@@ -287,6 +293,8 @@ public class VideoTaskPipelineServiceImpl implements VideoTaskPipelineService {
patch.setSceneImageKey(s.getSceneImageKey()); patch.setSceneImageKey(s.getSceneImageKey());
patch.setPropImageKeys(toJson(s.getPropImageKeys())); patch.setPropImageKeys(toJson(s.getPropImageKeys()));
patch.setStyleImageKey(s.getStyleImageKey()); patch.setStyleImageKey(s.getStyleImageKey());
patch.setFirstFrameImageKey(s.getFirstFrameImageKey());
patch.setLastFrameImageKey(s.getLastFrameImageKey());
patch.setDurationSeconds(durationOrDefault(s.getDurationSeconds())); patch.setDurationSeconds(durationOrDefault(s.getDurationSeconds()));
String editablePrompt = s.getFreeformPrompt() != null && !s.getFreeformPrompt().isBlank() String editablePrompt = s.getFreeformPrompt() != null && !s.getFreeformPrompt().isBlank()
? s.getFreeformPrompt() ? s.getFreeformPrompt()
...@@ -295,6 +303,17 @@ public class VideoTaskPipelineServiceImpl implements VideoTaskPipelineService { ...@@ -295,6 +303,17 @@ public class VideoTaskPipelineServiceImpl implements VideoTaskPipelineService {
storyboardMapper.updateById(patch); storyboardMapper.updateById(patch);
} }
private List<String> frameImageKeys(StructuredVideoSubmission s) {
List<String> keys = new ArrayList<>(2);
if (s.getFirstFrameImageKey() != null && !s.getFirstFrameImageKey().isBlank()) {
keys.add(s.getFirstFrameImageKey());
}
if (s.getLastFrameImageKey() != null && !s.getLastFrameImageKey().isBlank()) {
keys.add(s.getLastFrameImageKey());
}
return keys;
}
private Integer durationOrDefault(Integer durationSeconds) { private Integer durationOrDefault(Integer durationSeconds) {
if (durationSeconds == null || durationSeconds <= 0) { if (durationSeconds == null || durationSeconds <= 0) {
return 5; return 5;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment