feat: add seedance2 ai video compatibility
Some checks failed
StoryForge CI / Baseline checks (push) Has been cancelled
StoryForge CI / Backend tests (push) Has been cancelled
StoryForge CI / Web tests (push) Has been cancelled

This commit is contained in:
kris
2026-04-05 05:44:07 +08:00
parent b78d1eaa51
commit c61c12127f
8 changed files with 549 additions and 13 deletions

View File

@@ -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);

View File

@@ -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/);