Commit e7ad0c17 authored by yaoke.yk's avatar yaoke.yk

分镜页面优化生成视频,参数必填问题

parent d9a2baad
...@@ -39,6 +39,42 @@ const DEFAULT_VIDEO_MODEL = VIDEO_MODEL_OPTIONS[0].value; ...@@ -39,6 +39,42 @@ const DEFAULT_VIDEO_MODEL = VIDEO_MODEL_OPTIONS[0].value;
const MAX_CHARACTERS = 5; const MAX_CHARACTERS = 5;
const MAX_PROPS = 5; const MAX_PROPS = 5;
const PROMPT_PREVIEW_LIMIT = 80; const PROMPT_PREVIEW_LIMIT = 80;
// 分镜编辑器草稿存到 localStorage,按 (project, episode, storyboard) 分 key。
// 跨页面切换/刷新都不会丢;展开分镜时优先用草稿,没草稿才回退到上一次任务。
// v2: duration 默认 5s(之前 15s),audioOn 默认 true(之前 false)。bump 版本号让旧草稿失效。
const SB_DRAFT_KEY_PREFIX = "yaoai:sb-draft:v2";
const sbDraftKey = (pid: string, eid: string, sbId: string) =>
`${SB_DRAFT_KEY_PREFIX}:${pid}:${eid}:${sbId}`;
interface SbDraft {
shortDescription: string;
characterKeys: string[];
sceneKey: string;
propKeys: string[];
freeformPrompt: string;
selectedDuration: number;
selectedRatio: string;
selectedModel: string;
audioOn: boolean;
}
function loadSbDraft(key: string): SbDraft | null {
try {
const raw = localStorage.getItem(key);
return raw ? (JSON.parse(raw) as SbDraft) : null;
} catch {
return null;
}
}
function saveSbDraft(key: string, d: SbDraft) {
try {
localStorage.setItem(key, JSON.stringify(d));
} catch {
// quota / private mode — 静默失败
}
}
const FOCUS_RING = "focus:outline-none focus:ring-2 focus:ring-[#1c64ff]/20 focus:border-[#1c64ff]/45"; const FOCUS_RING = "focus:outline-none focus:ring-2 focus:ring-[#1c64ff]/20 focus:border-[#1c64ff]/45";
const CONTROL_SURFACE = "border border-slate-200/80 bg-white shadow-[0_1px_2px_rgba(15,23,42,0.04)]"; const CONTROL_SURFACE = "border border-slate-200/80 bg-white shadow-[0_1px_2px_rgba(15,23,42,0.04)]";
const SOFT_PANEL = "border border-slate-200/80 bg-white/95 shadow-[0_8px_24px_rgba(15,23,42,0.05)]"; const SOFT_PANEL = "border border-slate-200/80 bg-white/95 shadow-[0_8px_24px_rgba(15,23,42,0.05)]";
...@@ -97,10 +133,10 @@ export function StoryboardWorkspace() { ...@@ -97,10 +133,10 @@ export function StoryboardWorkspace() {
const [styleKey] = useState<string>(""); // reserved; not exposed in this UI const [styleKey] = useState<string>(""); // reserved; not exposed in this UI
const [shortDescription, setShortDescription] = useState<string>(""); const [shortDescription, setShortDescription] = useState<string>("");
const [freeformPrompt, setFreeformPrompt] = useState<string>(""); const [freeformPrompt, setFreeformPrompt] = useState<string>("");
const [selectedDuration, setSelectedDuration] = useState(15); const [selectedDuration, setSelectedDuration] = useState(5);
const [selectedRatio, setSelectedRatio] = useState<string>("16:9"); const [selectedRatio, setSelectedRatio] = useState<string>("16:9");
const [selectedModel, setSelectedModel] = useState<string>(DEFAULT_VIDEO_MODEL); const [selectedModel, setSelectedModel] = useState<string>(DEFAULT_VIDEO_MODEL);
const [audioOn, setAudioOn] = useState(false); const [audioOn, setAudioOn] = useState(true);
const [generatingVideoId, setGeneratingVideoId] = useState<string | null>(null); const [generatingVideoId, setGeneratingVideoId] = useState<string | null>(null);
// Modals // Modals
...@@ -147,6 +183,9 @@ export function StoryboardWorkspace() { ...@@ -147,6 +183,9 @@ export function StoryboardWorkspace() {
// 编辑器状态只在切换分镜卡片时从最近任务回填一次。 // 编辑器状态只在切换分镜卡片时从最近任务回填一次。
// 之后输入框完全归用户掌控,不会被 videoTasks 刷新(如点击生成后 mutation 失效)覆盖。 // 之后输入框完全归用户掌控,不会被 videoTasks 刷新(如点击生成后 mutation 失效)覆盖。
const lastSyncedExpandedIdRef = useRef<string | null>(null); const lastSyncedExpandedIdRef = useRef<string | null>(null);
// sync effect 触发 setStuff 后,本轮 render 内 save effect 会用陈旧 state 误存。
// 设这个 ref 让 save effect 跳过 sync 后的第一次触发,等下一轮 render 拿到新 state 再存。
const skipNextDraftSaveRef = useRef(false);
// Reset on episode change // Reset on episode change
useEffect(() => { useEffect(() => {
...@@ -173,7 +212,25 @@ export function StoryboardWorkspace() { ...@@ -173,7 +212,25 @@ export function StoryboardWorkspace() {
if (!videoTasksFetched) return; // 等首次拉到任务列表再回填,避免空数据回填后被锁 if (!videoTasksFetched) return; // 等首次拉到任务列表再回填,避免空数据回填后被锁
if (lastSyncedExpandedIdRef.current === expandedId) return; if (lastSyncedExpandedIdRef.current === expandedId) return;
lastSyncedExpandedIdRef.current = expandedId; lastSyncedExpandedIdRef.current = expandedId;
skipNextDraftSaveRef.current = true; // 跳过本轮陈旧 state 的误存
// 优先从 localStorage 草稿恢复(跨页面切换/刷新都不丢)
const dKey = pid && eid ? sbDraftKey(pid, eid, expanded.id) : null;
const draft = dKey ? loadSbDraft(dKey) : null;
if (draft) {
setShortDescription(draft.shortDescription);
setCharacterKeys(draft.characterKeys);
setSceneKey(draft.sceneKey);
setPropKeys(draft.propKeys);
setFreeformPrompt(draft.freeformPrompt);
setSelectedDuration(draft.selectedDuration);
setSelectedRatio(draft.selectedRatio);
setSelectedModel(draft.selectedModel);
setAudioOn(draft.audioOn);
return;
}
// 没草稿才回退到上一次任务
setShortDescription(expanded.shortDescription ?? ""); setShortDescription(expanded.shortDescription ?? "");
const lastTask = getTaskForSb(expanded); const lastTask = getTaskForSb(expanded);
const initialChars = lastTask?.characterImageKeys const initialChars = lastTask?.characterImageKeys
...@@ -187,6 +244,30 @@ export function StoryboardWorkspace() { ...@@ -187,6 +244,30 @@ export function StoryboardWorkspace() {
if (typeof lastTask?.generateAudio === "boolean") setAudioOn(lastTask.generateAudio); if (typeof lastTask?.generateAudio === "boolean") setAudioOn(lastTask.generateAudio);
}, [expandedId, videoTasksFetched]); }, [expandedId, videoTasksFetched]);
// 编辑器任意字段变化即写入 localStorage 草稿
useEffect(() => {
if (!expandedId || !pid || !eid) return;
if (skipNextDraftSaveRef.current) {
skipNextDraftSaveRef.current = false;
return;
}
saveSbDraft(sbDraftKey(pid, eid, expandedId), {
shortDescription,
characterKeys,
sceneKey,
propKeys,
freeformPrompt,
selectedDuration,
selectedRatio,
selectedModel,
audioOn,
});
}, [
expandedId, pid, eid,
shortDescription, characterKeys, sceneKey, propKeys, freeformPrompt,
selectedDuration, selectedRatio, selectedModel, audioOn,
]);
const handleEpisodeSelect = (ep: Episode) => { const handleEpisodeSelect = (ep: Episode) => {
setActiveEpisodeId(ep.id); setActiveEpisodeId(ep.id);
navigate(`/project/${pid}/storyboard/${ep.id}`, { replace: true }); navigate(`/project/${pid}/storyboard/${ep.id}`, { replace: true });
...@@ -211,7 +292,7 @@ export function StoryboardWorkspace() { ...@@ -211,7 +292,7 @@ export function StoryboardWorkspace() {
dialogues: "", dialogues: "",
cameraDirection: "", cameraDirection: "",
compositionGuide: "", compositionGuide: "",
durationSeconds: 15, durationSeconds: 5,
startFramePrompt: "", startFramePrompt: "",
motionScript: "", motionScript: "",
notes: "", notes: "",
...@@ -254,7 +335,9 @@ export function StoryboardWorkspace() { ...@@ -254,7 +335,9 @@ export function StoryboardWorkspace() {
}; };
const promptHasContent = freeformPrompt.trim().length > 0; const promptHasContent = freeformPrompt.trim().length > 0;
const canGenerate = !!expanded && characterKeys.length > 0 && !!sceneKey // 至少一张参考图(角色/场景/道具任一)+ 提示词非空
const hasAnyReferenceImage = characterKeys.length > 0 || !!sceneKey || propKeys.length > 0;
const canGenerate = !!expanded && hasAnyReferenceImage
&& promptHasContent && !generateStructured.isPending; && promptHasContent && !generateStructured.isPending;
const handleGenerate = async () => { const handleGenerate = async () => {
...@@ -526,7 +609,7 @@ export function StoryboardWorkspace() { ...@@ -526,7 +609,7 @@ export function StoryboardWorkspace() {
<button <button
onClick={handleGenerate} onClick={handleGenerate}
disabled={!canGenerate || generatingVideoId === expanded?.id} disabled={!canGenerate || generatingVideoId === expanded?.id}
title={!canGenerate ? "需要:至少 1 个出镜角色 + 场景图 + 提示词" : ""} title={!canGenerate ? "需要:至少 1 张参考图(角色/场景/道具任一)+ 提示词" : ""}
className="w-full flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl bg-[#1c64ff] text-white text-xs font-semibold tracking-wide shadow-[0_9px_20px_rgba(28,100,255,0.26)] transition-all duration-150 hover:bg-[#1857e8] hover:shadow-[0_12px_24px_rgba(28,100,255,0.32)] hover:-translate-y-px disabled:opacity-50 disabled:hover:translate-y-0 disabled:hover:shadow-none whitespace-nowrap" className="w-full flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl bg-[#1c64ff] text-white text-xs font-semibold tracking-wide shadow-[0_9px_20px_rgba(28,100,255,0.26)] transition-all duration-150 hover:bg-[#1857e8] hover:shadow-[0_12px_24px_rgba(28,100,255,0.32)] hover:-translate-y-px disabled:opacity-50 disabled:hover:translate-y-0 disabled:hover:shadow-none whitespace-nowrap"
> >
{generatingVideoId === expanded?.id ? ( {generatingVideoId === expanded?.id ? (
......
...@@ -23,9 +23,9 @@ public final class SeedancePromptBuilder { ...@@ -23,9 +23,9 @@ public final class SeedancePromptBuilder {
*/ */
/** V17:单段 freeform 提示词模式(前端单输入框后的主要入口)。 */ /** V17:单段 freeform 提示词模式(前端单输入框后的主要入口)。 */
public static String buildPromptFreeform(String freeformBody, int characterCount, public static String buildPromptFreeform(String freeformBody, int characterCount,
int propCount, boolean hasStyleImage) { boolean hasSceneImage, int propCount, boolean hasStyleImage) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
appendImageHeader(sb, characterCount, propCount, hasStyleImage); appendImageHeader(sb, characterCount, hasSceneImage, propCount, 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');
...@@ -35,9 +35,9 @@ public final class SeedancePromptBuilder { ...@@ -35,9 +35,9 @@ public final class SeedancePromptBuilder {
} }
public static String buildPrompt(UserPromptParts parts, int characterCount, public static String buildPrompt(UserPromptParts parts, int characterCount,
int propCount, boolean hasStyleImage) { boolean hasSceneImage, int propCount, boolean hasStyleImage) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
appendImageHeader(sb, characterCount, propCount, hasStyleImage); appendImageHeader(sb, characterCount, hasSceneImage, propCount, hasStyleImage);
sb.append("\n分镜内容:\n"); sb.append("\n分镜内容:\n");
appendIfPresent(sb, "人物动作", parts.characterAction()); appendIfPresent(sb, "人物动作", parts.characterAction());
...@@ -50,23 +50,30 @@ public final class SeedancePromptBuilder { ...@@ -50,23 +50,30 @@ public final class SeedancePromptBuilder {
return sb.toString(); return sb.toString();
} }
/**
* 根据实际存在的图类型动态分配 @图N 编号(角色 → 场景 → 道具 → 风格)。
* 任一类可缺省;全空时不输出图片说明。
*/
private static void appendImageHeader(StringBuilder sb, int characterCount, private static void appendImageHeader(StringBuilder sb, int characterCount,
int propCount, boolean hasStyleImage) { boolean hasSceneImage, int propCount, boolean hasStyleImage) {
int imageCount; int idx = 0;
if (characterCount <= 1) { if (characterCount == 1) {
sb.append("@图1 是角色参考图,请保持人物脸型、发型、服装、气质一致,不要随意改变角色身份。\n"); sb.append("@图").append(idx + 1)
imageCount = 1; .append(" 是角色参考图,请保持人物脸型、发型、服装、气质一致,不要随意改变角色身份。\n");
} else { idx += 1;
sb.append("@图1-").append(characterCount) } else if (characterCount > 1) {
sb.append("@图").append(idx + 1).append("-").append(idx + characterCount)
.append(" 是出镜角色参考图(按顺序对应剧本中第 1..").append(characterCount) .append(" 是出镜角色参考图(按顺序对应剧本中第 1..").append(characterCount)
.append(" 位角色),请保持每位人物的脸型、发型、服装、气质一致,不要混淆角色身份。\n"); .append(" 位角色),请保持每位人物的脸型、发型、服装、气质一致,不要混淆角色身份。\n");
imageCount = characterCount; idx += characterCount;
}
if (hasSceneImage) {
sb.append("@图").append(idx + 1)
.append(" 是场景参考图,请参考空间结构、环境布局、光影、氛围和构图。\n");
idx += 1;
} }
sb.append("@图").append(imageCount + 1)
.append(" 是场景参考图,请参考空间结构、环境布局、光影、氛围和构图。\n");
imageCount += 1;
if (propCount > 0) { if (propCount > 0) {
int propStart = imageCount + 1; int propStart = idx + 1;
int propEnd = propStart + propCount - 1; int propEnd = propStart + propCount - 1;
if (propCount == 1) { if (propCount == 1) {
sb.append("@图").append(propStart).append(" 是道具参考图,请在画面中合理使用。\n"); sb.append("@图").append(propStart).append(" 是道具参考图,请在画面中合理使用。\n");
...@@ -74,10 +81,10 @@ public final class SeedancePromptBuilder { ...@@ -74,10 +81,10 @@ public final class SeedancePromptBuilder {
sb.append("@图").append(propStart).append("-").append(propEnd) sb.append("@图").append(propStart).append("-").append(propEnd)
.append(" 是道具参考图,请在画面中合理使用这些道具。\n"); .append(" 是道具参考图,请在画面中合理使用这些道具。\n");
} }
imageCount += propCount; idx += propCount;
} }
if (hasStyleImage) { if (hasStyleImage) {
sb.append("@图").append(imageCount + 1) sb.append("@图").append(idx + 1)
.append(" 是风格参考图,请参考整体美术风格、色彩和质感。\n"); .append(" 是风格参考图,请参考整体美术风格、色彩和质感。\n");
} }
} }
...@@ -110,7 +117,9 @@ public final class SeedancePromptBuilder { ...@@ -110,7 +117,9 @@ public final class SeedancePromptBuilder {
} }
} }
} }
keys.add(sceneImageKey); if (sceneImageKey != null && !sceneImageKey.isBlank()) {
keys.add(sceneImageKey);
}
if (propImageKeys != null) { if (propImageKeys != null) {
for (String k : propImageKeys) { for (String k : propImageKeys) {
if (k != null && !k.isBlank()) { if (k != null && !k.isBlank()) {
......
...@@ -3,8 +3,6 @@ package com.yaoai.api.dto.ai; ...@@ -3,8 +3,6 @@ package com.yaoai.api.dto.ai;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min; import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size; import jakarta.validation.constraints.Size;
import lombok.Data; import lombok.Data;
...@@ -31,8 +29,7 @@ public class StructuredVideoGenerateRequest { ...@@ -31,8 +29,7 @@ public class StructuredVideoGenerateRequest {
@Size(max = 5, message = "出镜角色最多 5 个") @Size(max = 5, message = "出镜角色最多 5 个")
private List<String> characterImageKeys; private List<String> characterImageKeys;
/** @图2 场景图(必填) */ /** 场景图(可选)。角色/场景/道具至少传一种参考图,由 service 层校验。 */
@NotBlank(message = "场景图不能为空")
private String sceneImageKey; private String sceneImageKey;
/** 用户结构化提示词(旧版 5 段,与 freeformPrompt 二选一,至少一项非空) */ /** 用户结构化提示词(旧版 5 段,与 freeformPrompt 二选一,至少一项非空) */
......
...@@ -283,6 +283,9 @@ public class AssetGenPipelineServiceImpl implements AssetGenPipelineService { ...@@ -283,6 +283,9 @@ public class AssetGenPipelineServiceImpl implements AssetGenPipelineService {
throw new BizException(ErrorCode.NOT_FOUND, "场景不存在"); throw new BizException(ErrorCode.NOT_FOUND, "场景不存在");
} }
if (scene.getImagePrompt() == null || scene.getImagePrompt().isBlank()) { if (scene.getImagePrompt() == null || scene.getImagePrompt().isBlank()) {
scene.setImagePrompt(buildSceneImagePromptFallback(scene));
}
if (scene.getImagePrompt() == null || scene.getImagePrompt().isBlank()) {
throw new BizException(ErrorCode.INVALID_PARAM, "场景缺少图片 Prompt"); throw new BizException(ErrorCode.INVALID_PARAM, "场景缺少图片 Prompt");
} }
...@@ -358,6 +361,9 @@ public class AssetGenPipelineServiceImpl implements AssetGenPipelineService { ...@@ -358,6 +361,9 @@ public class AssetGenPipelineServiceImpl implements AssetGenPipelineService {
@Override @Override
public Scene saveScene(Scene scene) { public Scene saveScene(Scene scene) {
if (scene.getImagePrompt() == null || scene.getImagePrompt().isBlank()) {
scene.setImagePrompt(buildSceneImagePromptFallback(scene));
}
if (scene.getId() == null) { if (scene.getId() == null) {
sceneMapper.insert(scene); sceneMapper.insert(scene);
} else { } else {
...@@ -366,6 +372,29 @@ public class AssetGenPipelineServiceImpl implements AssetGenPipelineService { ...@@ -366,6 +372,29 @@ public class AssetGenPipelineServiceImpl implements AssetGenPipelineService {
return scene; return scene;
} }
private String buildSceneImagePromptFallback(Scene scene) {
if (scene == null) {
return "";
}
StringBuilder prompt = new StringBuilder();
if (scene.getName() != null && !scene.getName().isBlank()) {
prompt.append(scene.getName().trim());
}
if (scene.getDescription() != null && !scene.getDescription().isBlank()) {
if (prompt.length() > 0) {
prompt.append(", ");
}
prompt.append(scene.getDescription().trim());
}
if (scene.getSceneType() != null && !scene.getSceneType().isBlank()) {
if (prompt.length() > 0) {
prompt.append(", ");
}
prompt.append("indoor".equalsIgnoreCase(scene.getSceneType()) ? "indoor scene" : "outdoor scene");
}
return prompt.toString();
}
@Override @Override
public void deleteCharacter(Long id, Long tenantId) { public void deleteCharacter(Long id, Long tenantId) {
Character character = characterMapper.selectById(id); Character character = characterMapper.selectById(id);
......
...@@ -140,22 +140,26 @@ public class VideoTaskPipelineServiceImpl implements VideoTaskPipelineService { ...@@ -140,22 +140,26 @@ public class VideoTaskPipelineServiceImpl implements VideoTaskPipelineService {
// 角色:优先使用 characterImageKeys 列表,回退到旧单图字段 // 角色:优先使用 characterImageKeys 列表,回退到旧单图字段
List<String> characterKeys = s.getCharacterImageKeys(); List<String> characterKeys = s.getCharacterImageKeys();
if (characterKeys == null || characterKeys.isEmpty()) { if (characterKeys == null || characterKeys.isEmpty()) {
if (s.getCharacterImageKey() == null || s.getCharacterImageKey().isBlank()) { if (s.getCharacterImageKey() != null && !s.getCharacterImageKey().isBlank()) {
throw new BizException(ErrorCode.INVALID_PARAM, "角色图不能为空"); characterKeys = java.util.List.of(s.getCharacterImageKey());
} else {
characterKeys = Collections.emptyList();
} }
characterKeys = java.util.List.of(s.getCharacterImageKey());
} }
if (s.getSceneImageKey() == null || s.getSceneImageKey().isBlank()) { boolean hasScene = s.getSceneImageKey() != null && !s.getSceneImageKey().isBlank();
throw new BizException(ErrorCode.INVALID_PARAM, "场景图不能为空"); int propCount = s.getPropImageKeys() != null ? s.getPropImageKeys().size() : 0;
boolean hasStyle = s.getStyleImageKey() != null && !s.getStyleImageKey().isBlank();
// 至少要有一张参考图(角色/场景/道具任一),否则前端不应允许提交
if (characterKeys.isEmpty() && !hasScene && propCount == 0) {
throw new BizException(ErrorCode.INVALID_PARAM, "至少需要一张参考图(角色/场景/道具任一)");
} }
// 1. 拼装 final prompt + 图片顺序 // 1. 拼装 final prompt + 图片顺序
int propCount = s.getPropImageKeys() != null ? s.getPropImageKeys().size() : 0;
boolean hasStyle = s.getStyleImageKey() != null && !s.getStyleImageKey().isBlank();
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(), propCount, hasStyle); s.getFreeformPrompt(), characterKeys.size(), hasScene, propCount, hasStyle);
} else { } else {
finalPrompt = SeedancePromptBuilder.buildPrompt( finalPrompt = SeedancePromptBuilder.buildPrompt(
new SeedancePromptBuilder.UserPromptParts( new SeedancePromptBuilder.UserPromptParts(
...@@ -166,6 +170,7 @@ public class VideoTaskPipelineServiceImpl implements VideoTaskPipelineService { ...@@ -166,6 +170,7 @@ public class VideoTaskPipelineServiceImpl implements VideoTaskPipelineService {
s.getVideoStyle() s.getVideoStyle()
), ),
characterKeys.size(), characterKeys.size(),
hasScene,
propCount, propCount,
hasStyle hasStyle
); );
...@@ -186,7 +191,9 @@ public class VideoTaskPipelineServiceImpl implements VideoTaskPipelineService { ...@@ -186,7 +191,9 @@ public class VideoTaskPipelineServiceImpl implements VideoTaskPipelineService {
task.setTaskType("structured_video"); task.setTaskType("structured_video");
task.setStatus("pending"); task.setStatus("pending");
task.setPrompt(finalPrompt); task.setPrompt(finalPrompt);
task.setCharacterImageKey(characterKeys.get(0)); if (!characterKeys.isEmpty()) {
task.setCharacterImageKey(characterKeys.get(0));
}
task.setCharacterImageKeys(toJson(characterKeys)); task.setCharacterImageKeys(toJson(characterKeys));
task.setSceneImageKey(s.getSceneImageKey()); task.setSceneImageKey(s.getSceneImageKey());
task.setPropImageKeys(toJson(s.getPropImageKeys())); task.setPropImageKeys(toJson(s.getPropImageKeys()));
......
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