feat: add seedance2 ai video compatibility
This commit is contained in:
@@ -10917,21 +10917,86 @@ function openCreateAiVideoAction(defaults = {}) {
|
||||
const assistant = getSelectedAssistant();
|
||||
const kb = getProjectKnowledgeBases(project.id)[0];
|
||||
const sourceJob = defaults.sourceJob || null;
|
||||
const defaultVideoProvider = String(
|
||||
defaults.videoProvider || defaults.video_provider || sourceJob?.artifacts?.video_provider || "doubao"
|
||||
).trim() || "doubao";
|
||||
const defaultVideoModel = String(
|
||||
defaults.videoModel || defaults.video_model || sourceJob?.artifacts?.video_model || ""
|
||||
).trim() || (defaultVideoProvider === "seedance2" ? "seedance-2.0-pro" : "");
|
||||
openActionModal({
|
||||
title: "创建 AI 视频任务",
|
||||
description: "输入 brief 后,直接触发 AI 视频链。",
|
||||
description: "输入 brief 后,直接触发 AI 视频链。需要更强镜头语言时,可以切到 Seedance 2.0。",
|
||||
submitLabel: "开始生产",
|
||||
fields: [
|
||||
{ name: "title", label: "任务标题", value: defaults.title || (sourceJob ? `${sourceJob.title} · AI 视频` : ""), placeholder: "例如:创业口播 AI 视频测试" },
|
||||
{ name: "brief", label: "视频 brief", type: "textarea", rows: 5, value: defaults.brief || getJobSeedBrief(sourceJob), placeholder: "写明主题、风格、镜头和目标受众" },
|
||||
{ name: "sourceJobId", label: "关联源任务", type: "select", value: defaults.sourceJobId || sourceJob?.id || "", options: [{ value: "", label: "不关联" }, ...getCompletedJobOptions()] },
|
||||
{
|
||||
name: "videoProvider",
|
||||
label: "视频引擎",
|
||||
type: "select",
|
||||
value: defaultVideoProvider,
|
||||
options: [
|
||||
{ value: "doubao", label: "当前默认引擎" },
|
||||
{ value: "seedance2", label: "Seedance 2.0" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "videoModel",
|
||||
label: "引擎模型",
|
||||
value: defaultVideoModel,
|
||||
placeholder: "例如:seedance-2.0-pro",
|
||||
},
|
||||
{ name: "style", label: "风格", value: defaults.style || "realistic" },
|
||||
{
|
||||
name: "aspectRatio",
|
||||
label: "画幅",
|
||||
type: "select",
|
||||
value: defaults.aspectRatio || defaults.aspect_ratio || sourceJob?.artifacts?.aspect_ratio || "9:16",
|
||||
options: [
|
||||
{ value: "9:16", label: "9:16 竖屏" },
|
||||
{ value: "16:9", label: "16:9 横屏" },
|
||||
{ value: "1:1", label: "1:1 方形" },
|
||||
],
|
||||
},
|
||||
{ name: "shots", label: "镜头数", type: "number", value: defaults.shots || 4, min: 1, max: 12 },
|
||||
{ name: "duration", label: "单镜头秒数", type: "number", value: defaults.duration || 5, min: 3, max: 12 }
|
||||
{ name: "duration", label: "单镜头秒数", type: "number", value: defaults.duration || 5, min: 3, max: 12 },
|
||||
{
|
||||
name: "cameraLanguage",
|
||||
label: "镜头语言",
|
||||
type: "textarea",
|
||||
rows: 3,
|
||||
value: defaults.cameraLanguage || defaults.camera_language || "",
|
||||
placeholder: "例如:开场推近,中段快速切换,结尾定格主卖点",
|
||||
},
|
||||
{
|
||||
name: "motionRhythm",
|
||||
label: "运动节奏",
|
||||
value: defaults.motionRhythm || defaults.motion_rhythm || "",
|
||||
placeholder: "例如:强节奏推进,镜头切换干净利落",
|
||||
},
|
||||
{
|
||||
name: "visualGuardrails",
|
||||
label: "风格约束",
|
||||
type: "textarea",
|
||||
rows: 3,
|
||||
value: defaults.visualGuardrails || defaults.visual_guardrails || "",
|
||||
placeholder: "例如:避免过暗,人物手部自然,保持真实商业质感",
|
||||
}
|
||||
],
|
||||
onSubmit: async (values) => {
|
||||
if (!values.title?.trim()) throw new Error("请填写任务标题");
|
||||
if (!values.brief?.trim()) throw new Error("请填写视频 brief");
|
||||
const normalizedProvider = String(values.videoProvider || "doubao").trim() || "doubao";
|
||||
const normalizedVideoModel = String(values.videoModel || "").trim() || (normalizedProvider === "seedance2" ? "seedance-2.0-pro" : "");
|
||||
const seedanceSuffix = normalizedProvider === "seedance2"
|
||||
? [
|
||||
values.cameraLanguage?.trim() ? `镜头语言:${values.cameraLanguage.trim()}` : "",
|
||||
values.motionRhythm?.trim() ? `运动节奏:${values.motionRhythm.trim()}` : "",
|
||||
values.visualGuardrails?.trim() ? `风格约束:${values.visualGuardrails.trim()}` : "",
|
||||
].filter(Boolean).join("\n")
|
||||
: "";
|
||||
const finalBrief = [values.brief.trim(), seedanceSuffix].filter(Boolean).join("\n\n");
|
||||
const job = await storyforgeFetch("/v2/pipelines/ai-video", {
|
||||
method: "POST",
|
||||
body: {
|
||||
@@ -10940,13 +11005,21 @@ function openCreateAiVideoAction(defaults = {}) {
|
||||
knowledge_base_id: kb?.id || "",
|
||||
source_job_id: values.sourceJobId || "",
|
||||
title: values.title.trim(),
|
||||
brief: values.brief.trim(),
|
||||
brief: finalBrief,
|
||||
style: values.style || "realistic",
|
||||
aspect_ratio: values.aspectRatio || "9:16",
|
||||
shots: Number(values.shots || 4),
|
||||
duration: Number(values.duration || 5)
|
||||
duration: Number(values.duration || 5),
|
||||
video_provider: values.videoProvider || "doubao",
|
||||
video_model: values.videoModel || "",
|
||||
}
|
||||
});
|
||||
rememberAction("AI 视频任务已创建", `已创建任务 ${job.title || job.id}。`, "blue", job);
|
||||
rememberAction(
|
||||
"AI 视频任务已创建",
|
||||
`已创建任务 ${job.title || job.id},引擎 ${normalizedProvider === "seedance2" ? "Seedance 2.0" : "当前默认引擎"}。`,
|
||||
"blue",
|
||||
job
|
||||
);
|
||||
await bootstrap();
|
||||
if (job?.id) {
|
||||
openJobDetailAction(job.id);
|
||||
|
||||
@@ -521,6 +521,18 @@ test("projects screen uses an adaptive project grid instead of a fixed three-col
|
||||
assert.match(CSS, /\.entity-card\.pad\s*\{[\s\S]*padding:\s*15px/);
|
||||
});
|
||||
|
||||
test("ai video action exposes seedance provider controls and sends provider metadata", () => {
|
||||
const source = extractBetween(APP, "function openCreateAiVideoAction(defaults = {})", "function openCreateRealCutAction(");
|
||||
assert.match(source, /视频引擎/);
|
||||
assert.match(source, /seedance2/);
|
||||
assert.match(source, /seedance-2\.0-pro/);
|
||||
assert.match(source, /cameraLanguage/);
|
||||
assert.match(source, /motionRhythm/);
|
||||
assert.match(source, /visualGuardrails/);
|
||||
assert.match(source, /video_provider:\s*values\.videoProvider/);
|
||||
assert.match(source, /video_model:\s*values\.videoModel/);
|
||||
});
|
||||
|
||||
test("mobile typography clamps subtitles and dense card copy on small screens", () => {
|
||||
assert.match(CSS, /@media \(max-width: 760px\)[\s\S]*\.panel-subtitle\s*\{[\s\S]*-webkit-line-clamp:\s*2/);
|
||||
assert.match(CSS, /@media \(max-width: 760px\)[\s\S]*\.task-item p,[\s\S]*\.review-card p\s*\{[\s\S]*-webkit-line-clamp:\s*3/);
|
||||
|
||||
Reference in New Issue
Block a user