From d0ae34ae4a5ae468a6c868ceabc2a03ff895dfcb Mon Sep 17 00:00:00 2001 From: kris Date: Sun, 5 Apr 2026 12:28:13 +0800 Subject: [PATCH] feat: recommend smarter defaults for creative sheets --- CHANGELOG.md | 6 ++ web/storyforge-web-v4/assets/app.js | 63 ++++++++++++++++--- .../tests/workbench-pages.test.mjs | 15 ++++- 3 files changed, 76 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5997841..7ee949f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,12 @@ - `生成文案` 和 `写复盘` 也会优先继承来源任务的平台,避免用户再手工改一次平台。 - 这样从任务详情或主 Agent 结果卡继续往下做时,表单第一眼就知道自己承接的是哪条任务。 +### 高优先级创作表单开始自动推荐更合理的默认值 + +- `生成文案` 现在会按当前平台自动给出更合适的默认受众,而不再一律写成“创业者”。 +- `创建 AI 视频` 会按来源任务自动推荐风格、画幅和单镜头时长;`创建实拍剪辑` 会自动推荐目标时长和画幅。 +- 这样从主 Agent、任务详情或最近完成任务继续往下做时,表单默认值会更贴近当前任务本身,而不是每次都从通用模板起步。 + ### 主 Agent 抖音相似搜索与对标关系 live 修复 - 修复 `search-similar-accounts` / `save-benchmark-link` 在抖音 live 数据上错误按 `project_id` 查询账号导致的 500。 diff --git a/web/storyforge-web-v4/assets/app.js b/web/storyforge-web-v4/assets/app.js index e7c4558..922ef4a 100644 --- a/web/storyforge-web-v4/assets/app.js +++ b/web/storyforge-web-v4/assets/app.js @@ -384,6 +384,52 @@ function renderSourceJobContextHtml(job) { `; } +function recommendAudienceForPlatform(platform) { + const normalized = normalizePlatformValue(platform, "douyin"); + if (normalized === "bilibili") return "深度内容观众"; + if (normalized === "xiaohongshu") return "兴趣消费人群"; + if (normalized === "kuaishou") return "成交导向受众"; + if (normalized === "wechat_video") return "私域关系链用户"; + return "创业者"; +} + +function recommendAspectRatioForPlatform(platform) { + const normalized = normalizePlatformValue(platform, "douyin"); + return normalized === "bilibili" ? "16:9" : "9:16"; +} + +function recommendCreativeStyle(sourceJob) { + const style = String(sourceJob?.artifacts?.style || sourceJob?.style || "").trim(); + if (style) return style; + if (String(sourceJob?.job_type || "").includes("review")) return "structured"; + return "realistic"; +} + +function recommendRealCutDuration(sourceJob) { + const candidate = Number( + sourceJob?.artifacts?.target_duration_sec + || sourceJob?.artifacts?.duration + || sourceJob?.result?.target_duration_sec + || 0 + ); + if (Number.isFinite(candidate) && candidate > 0) { + return Math.max(10, Math.min(300, Math.round(candidate))); + } + return 60; +} + +function recommendAiVideoShotDuration(sourceJob) { + const candidate = Number( + sourceJob?.artifacts?.duration + || sourceJob?.result?.duration + || 0 + ); + if (Number.isFinite(candidate) && candidate > 0) { + return Math.max(3, Math.min(12, Math.round(candidate))); + } + return 5; +} + function formatDateTime(value) { if (!value) return "-"; const date = new Date(value); @@ -11328,6 +11374,7 @@ function openGenerateCopyAction(defaults = {}) { const assistant = getSelectedAssistant() || requireSelectedAssistant(); const sourceJob = defaults.sourceJob || null; const project = getSelectedProject(); + const defaultPlatform = normalizePlatformValue(defaults.platform || sourceJob?.platform || "douyin"); openActionModal({ title: "生成文案", description: "用当前 Agent 和知识库生成一版短视频文案。", @@ -11336,8 +11383,8 @@ function openGenerateCopyAction(defaults = {}) { { name: "context", label: "当前上下文", type: "html", html: renderIntakeActionContextHtml(project?.id || "", assistant.id) }, ...(sourceJob ? [{ name: "sourceJobContext", label: "来源任务", type: "html", html: renderSourceJobContextHtml(sourceJob) }] : []), { name: "brief", label: "创作需求", type: "textarea", rows: 5, value: defaults.brief || getJobSeedBrief(sourceJob), placeholder: "例如:给创业者写一条 60 字内的短视频开场文案" }, - { name: "platform", label: "平台", type: "select", value: normalizePlatformValue(defaults.platform || sourceJob?.platform || "douyin"), options: getPlatformOptions() }, - { name: "audience", label: "受众", value: "创业者" }, + { name: "platform", label: "平台", type: "select", value: defaultPlatform, options: getPlatformOptions() }, + { name: "audience", label: "受众", value: defaults.audience || recommendAudienceForPlatform(defaultPlatform) }, { name: "extraRequirements", label: "额外要求", placeholder: "例如:强结论开头,结尾带 CTA" } ], onSubmit: async (values) => { @@ -11376,6 +11423,7 @@ function openCreateAiVideoAction(defaults = {}) { const kb = getProjectKnowledgeBases(project.id)[0]; const defaultAssistantId = assistant?.id || ""; const sourceJob = defaults.sourceJob || null; + const defaultPlatform = normalizePlatformValue(defaults.platform || sourceJob?.platform || "douyin"); const defaultVideoProvider = String( defaults.videoProvider || defaults.video_provider || sourceJob?.artifacts?.video_provider || "doubao" ).trim() || "doubao"; @@ -11408,12 +11456,12 @@ function openCreateAiVideoAction(defaults = {}) { value: defaultVideoModel, placeholder: "例如:seedance-2.0-pro", }, - { name: "style", label: "风格", value: defaults.style || "realistic" }, + { name: "style", label: "风格", value: defaults.style || recommendCreativeStyle(sourceJob) }, { name: "aspectRatio", label: "画幅", type: "select", - value: defaults.aspectRatio || defaults.aspect_ratio || sourceJob?.artifacts?.aspect_ratio || "9:16", + value: defaults.aspectRatio || defaults.aspect_ratio || sourceJob?.artifacts?.aspect_ratio || recommendAspectRatioForPlatform(defaultPlatform), options: [ { value: "9:16", label: "9:16 竖屏" }, { value: "16:9", label: "16:9 横屏" }, @@ -11421,7 +11469,7 @@ function openCreateAiVideoAction(defaults = {}) { ], }, { 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 || recommendAiVideoShotDuration(sourceJob), min: 3, max: 12 }, { name: "cameraLanguage", label: "镜头语言", @@ -11498,6 +11546,7 @@ function openCreateRealCutAction(defaults = {}) { const project = requireSelectedProject(); const sourceJob = defaults.sourceJob || null; const assistant = getSelectedAssistant(); + const defaultPlatform = normalizePlatformValue(defaults.platform || sourceJob?.platform || "douyin"); openActionModal({ title: "创建实拍剪辑任务", description: "基于已完成的源任务,把素材发到 cutvideo。", @@ -11507,8 +11556,8 @@ function openCreateRealCutAction(defaults = {}) { ...(sourceJob ? [{ name: "sourceJobContext", label: "来源任务", type: "html", html: renderSourceJobContextHtml(sourceJob) }] : []), { name: "title", label: "任务标题", value: defaults.title || (sourceJob ? `${sourceJob.title} · 实拍剪辑` : ""), placeholder: "例如:创业素材粗剪" }, { name: "sourceJobId", label: "源任务", type: "select", value: defaults.sourceJobId || sourceJob?.id || getCompletedJobOptions()[0]?.value || "", options: getCompletedJobOptions() }, - { name: "targetDurationSec", label: "目标时长(秒)", type: "number", value: defaults.targetDurationSec || 60, min: 10, max: 300 }, - { name: "aspectRatio", label: "画幅", value: defaults.aspectRatio || "9:16" }, + { name: "targetDurationSec", label: "目标时长(秒)", type: "number", value: defaults.targetDurationSec || recommendRealCutDuration(sourceJob), min: 10, max: 300 }, + { name: "aspectRatio", label: "画幅", value: defaults.aspectRatio || sourceJob?.artifacts?.aspect_ratio || recommendAspectRatioForPlatform(defaultPlatform) }, { name: "objective", label: "目标", type: "textarea", rows: 4, value: defaults.objective || "", placeholder: "例如:保留高信息密度片段,输出适合短视频平台的粗剪结果" } ], onSubmit: async (values) => { diff --git a/web/storyforge-web-v4/tests/workbench-pages.test.mjs b/web/storyforge-web-v4/tests/workbench-pages.test.mjs index 81f4bb4..1769822 100644 --- a/web/storyforge-web-v4/tests/workbench-pages.test.mjs +++ b/web/storyforge-web-v4/tests/workbench-pages.test.mjs @@ -1148,6 +1148,11 @@ test("input-heavy intake sheets surface current context and smarter defaults", ( const reviewAction = extractBetween(APP, "function openReviewAction(defaults = {})", "document.addEventListener(\"click\", async (event) => {"); assert.match(APP, /function renderIntakeActionContextHtml\(/); assert.match(APP, /function renderSourceJobContextHtml\(job\)/); + assert.match(APP, /function recommendAudienceForPlatform\(platform\)/); + assert.match(APP, /function recommendAspectRatioForPlatform\(platform\)/); + assert.match(APP, /function recommendCreativeStyle\(sourceJob\)/); + assert.match(APP, /function recommendRealCutDuration\(sourceJob\)/); + assert.match(APP, /function recommendAiVideoShotDuration\(sourceJob\)/); assert.match(importHomepage, /label: "当前上下文", type: "html"/); assert.match(importHomepage, /const defaultAssistantId = getSelectedAssistant\(\)\?\.id \|\| assistants\[0\]\?\.value \|\| ""/); assert.match(importHomepage, /renderIntakeActionContextHtml\(project\.id, defaultAssistantId\)/); @@ -1173,14 +1178,22 @@ test("input-heavy intake sheets surface current context and smarter defaults", ( assert.match(generateCopy, /label: "当前上下文", type: "html"/); assert.match(generateCopy, /renderIntakeActionContextHtml\(project\?\.id \|\| "", assistant\.id\)/); assert.match(generateCopy, /sourceJob \? \[\{ name: "sourceJobContext", label: "来源任务", type: "html", html: renderSourceJobContextHtml\(sourceJob\) \}\] : \[\]/); - assert.match(generateCopy, /normalizePlatformValue\(defaults\.platform \|\| sourceJob\?\.platform \|\| "douyin"\)/); + assert.match(generateCopy, /const defaultPlatform = normalizePlatformValue\(defaults\.platform \|\| sourceJob\?\.platform \|\| "douyin"\)/); + assert.match(generateCopy, /value: defaults\.audience \|\| recommendAudienceForPlatform\(defaultPlatform\)/); assert.match(createAiVideo, /label: "当前上下文", type: "html"/); assert.match(createAiVideo, /const defaultAssistantId = assistant\?\.id \|\| ""/); + assert.match(createAiVideo, /const defaultPlatform = normalizePlatformValue\(defaults\.platform \|\| sourceJob\?\.platform \|\| "douyin"\)/); assert.match(createAiVideo, /renderIntakeActionContextHtml\(project\.id, defaultAssistantId\)/); assert.match(createAiVideo, /sourceJob \? \[\{ name: "sourceJobContext", label: "来源任务", type: "html", html: renderSourceJobContextHtml\(sourceJob\) \}\] : \[\]/); + assert.match(createAiVideo, /value: defaults\.style \|\| recommendCreativeStyle\(sourceJob\)/); + assert.match(createAiVideo, /recommendAspectRatioForPlatform\(defaultPlatform\)/); + assert.match(createAiVideo, /value: defaults\.duration \|\| recommendAiVideoShotDuration\(sourceJob\)/); assert.match(createRealCut, /label: "当前上下文", type: "html"/); + assert.match(createRealCut, /const defaultPlatform = normalizePlatformValue\(defaults\.platform \|\| sourceJob\?\.platform \|\| "douyin"\)/); assert.match(createRealCut, /renderIntakeActionContextHtml\(project\.id, assistant\?\.id \|\| ""\)/); assert.match(createRealCut, /sourceJob \? \[\{ name: "sourceJobContext", label: "来源任务", type: "html", html: renderSourceJobContextHtml\(sourceJob\) \}\] : \[\]/); + assert.match(createRealCut, /value: defaults\.targetDurationSec \|\| recommendRealCutDuration\(sourceJob\)/); + assert.match(createRealCut, /recommendAspectRatioForPlatform\(defaultPlatform\)/); assert.match(reviewAction, /label: "当前上下文", type: "html"/); assert.match(reviewAction, /const defaultAssistantId = getSelectedAssistant\(\)\?\.id \|\| assistants\[0\]\?\.value \|\| ""/); assert.match(reviewAction, /renderIntakeActionContextHtml\(project\.id, defaultAssistantId\)/);