From 571be6957104729e51d563e65f80938c76392490 Mon Sep 17 00:00:00 2001 From: kris Date: Sun, 5 Apr 2026 12:31:21 +0800 Subject: [PATCH] feat: recommend task-aware defaults for creative forms --- CHANGELOG.md | 6 +++++ web/storyforge-web-v4/assets/app.js | 22 +++++++++++++++---- .../tests/workbench-pages.test.mjs | 6 +++++ 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ee949f..cdcef7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,12 @@ - `创建 AI 视频` 会按来源任务自动推荐风格、画幅和单镜头时长;`创建实拍剪辑` 会自动推荐目标时长和画幅。 - 这样从主 Agent、任务详情或最近完成任务继续往下做时,表单默认值会更贴近当前任务本身,而不是每次都从通用模板起步。 +### 高优先级创作表单开始自动补标题和剪辑目标 + +- `创建 AI 视频 / 创建实拍剪辑 / 写复盘` 现在会优先基于来源任务自动带出更合理的标题,而不是总让用户自己再补一遍。 +- `创建实拍剪辑` 还会基于来源任务摘要自动生成更贴近当前任务的默认剪辑目标。 +- 这样从某条任务继续派生后续动作时,表单不仅默认值更合理,连标题和目标文案也更像是承接当前任务的自然下一步。 + ### 主 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 922ef4a..0f9c8d7 100644 --- a/web/storyforge-web-v4/assets/app.js +++ b/web/storyforge-web-v4/assets/app.js @@ -430,6 +430,20 @@ function recommendAiVideoShotDuration(sourceJob) { return 5; } +function recommendDerivativeJobTitle(sourceJob, suffix, fallback) { + const base = String(sourceJob?.title || "").trim(); + if (base) return `${base} · ${suffix}`; + return fallback; +} + +function recommendRealCutObjective(sourceJob) { + const briefText = brief(getJobSeedBrief(sourceJob), 48); + if (briefText && briefText !== "暂无") { + return `围绕「${briefText}」保留高信息密度片段,输出适合短视频平台的粗剪结果`; + } + return "保留高信息密度片段,输出适合短视频平台的粗剪结果"; +} + function formatDateTime(value) { if (!value) return "-"; const date = new Date(value); @@ -11437,7 +11451,7 @@ function openCreateAiVideoAction(defaults = {}) { fields: [ { name: "context", label: "当前上下文", type: "html", html: renderIntakeActionContextHtml(project.id, defaultAssistantId) }, ...(sourceJob ? [{ name: "sourceJobContext", label: "来源任务", type: "html", html: renderSourceJobContextHtml(sourceJob) }] : []), - { name: "title", label: "任务标题", value: defaults.title || (sourceJob ? `${sourceJob.title} · AI 视频` : ""), placeholder: "例如:创业口播 AI 视频测试" }, + { name: "title", label: "任务标题", value: defaults.title || recommendDerivativeJobTitle(sourceJob, "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()] }, { @@ -11554,11 +11568,11 @@ function openCreateRealCutAction(defaults = {}) { fields: [ { name: "context", label: "当前上下文", type: "html", html: renderIntakeActionContextHtml(project.id, assistant?.id || "") }, ...(sourceJob ? [{ name: "sourceJobContext", label: "来源任务", type: "html", html: renderSourceJobContextHtml(sourceJob) }] : []), - { name: "title", label: "任务标题", value: defaults.title || (sourceJob ? `${sourceJob.title} · 实拍剪辑` : ""), placeholder: "例如:创业素材粗剪" }, + { name: "title", label: "任务标题", value: defaults.title || recommendDerivativeJobTitle(sourceJob, "实拍剪辑", ""), placeholder: "例如:创业素材粗剪" }, { name: "sourceJobId", label: "源任务", type: "select", value: defaults.sourceJobId || sourceJob?.id || getCompletedJobOptions()[0]?.value || "", options: getCompletedJobOptions() }, { 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: "例如:保留高信息密度片段,输出适合短视频平台的粗剪结果" } + { name: "objective", label: "目标", type: "textarea", rows: 4, value: defaults.objective || recommendRealCutObjective(sourceJob), placeholder: "例如:保留高信息密度片段,输出适合短视频平台的粗剪结果" } ], onSubmit: async (values) => { if (!values.title?.trim()) throw new Error("请填写任务标题"); @@ -11819,7 +11833,7 @@ function openReviewAction(defaults = {}) { fields: [ { name: "context", label: "当前上下文", type: "html", html: renderIntakeActionContextHtml(project.id, defaultAssistantId) }, ...(sourceJob ? [{ name: "sourceJobContext", label: "来源任务", type: "html", html: renderSourceJobContextHtml(sourceJob) }] : []), - { name: "title", label: "标题", value: existingReview?.title || defaults.title || sourceJob?.title || "", placeholder: "例如:创业口播 3 月 22 日复盘" }, + { name: "title", label: "标题", value: existingReview?.title || defaults.title || recommendDerivativeJobTitle(sourceJob, "复盘", ""), placeholder: "例如:创业口播 3 月 22 日复盘" }, { name: "sourceJobId", label: "关联任务", type: "select", value: existingReview?.source_job_id || defaults.sourceJobId || sourceJob?.id || "", options: [{ value: "", label: "不关联任务" }, ...getCompletedJobOptions()] }, { name: "assistantId", label: "负责 Agent", type: "select", value: existingReview?.assistant_id || defaultAssistantId, options: [{ value: "", label: "先不绑定" }, ...assistants] }, { name: "platform", label: "平台", type: "select", value: normalizePlatformValue(existingReview?.platform || defaults.platform || sourceJob?.platform || "douyin"), options: getPlatformOptions() }, diff --git a/web/storyforge-web-v4/tests/workbench-pages.test.mjs b/web/storyforge-web-v4/tests/workbench-pages.test.mjs index 1769822..2d27fde 100644 --- a/web/storyforge-web-v4/tests/workbench-pages.test.mjs +++ b/web/storyforge-web-v4/tests/workbench-pages.test.mjs @@ -1153,6 +1153,8 @@ test("input-heavy intake sheets surface current context and smarter defaults", ( assert.match(APP, /function recommendCreativeStyle\(sourceJob\)/); assert.match(APP, /function recommendRealCutDuration\(sourceJob\)/); assert.match(APP, /function recommendAiVideoShotDuration\(sourceJob\)/); + assert.match(APP, /function recommendDerivativeJobTitle\(sourceJob, suffix, fallback\)/); + assert.match(APP, /function recommendRealCutObjective\(sourceJob\)/); assert.match(importHomepage, /label: "当前上下文", type: "html"/); assert.match(importHomepage, /const defaultAssistantId = getSelectedAssistant\(\)\?\.id \|\| assistants\[0\]\?\.value \|\| ""/); assert.match(importHomepage, /renderIntakeActionContextHtml\(project\.id, defaultAssistantId\)/); @@ -1185,6 +1187,7 @@ test("input-heavy intake sheets surface current context and smarter defaults", ( 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\.title \|\| recommendDerivativeJobTitle\(sourceJob, "AI 视频", ""\)/); assert.match(createAiVideo, /value: defaults\.style \|\| recommendCreativeStyle\(sourceJob\)/); assert.match(createAiVideo, /recommendAspectRatioForPlatform\(defaultPlatform\)/); assert.match(createAiVideo, /value: defaults\.duration \|\| recommendAiVideoShotDuration\(sourceJob\)/); @@ -1192,12 +1195,15 @@ test("input-heavy intake sheets surface current context and smarter defaults", ( 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\.title \|\| recommendDerivativeJobTitle\(sourceJob, "实拍剪辑", ""\)/); assert.match(createRealCut, /value: defaults\.targetDurationSec \|\| recommendRealCutDuration\(sourceJob\)/); assert.match(createRealCut, /recommendAspectRatioForPlatform\(defaultPlatform\)/); + assert.match(createRealCut, /value: defaults\.objective \|\| recommendRealCutObjective\(sourceJob\)/); assert.match(reviewAction, /label: "当前上下文", type: "html"/); assert.match(reviewAction, /const defaultAssistantId = getSelectedAssistant\(\)\?\.id \|\| assistants\[0\]\?\.value \|\| ""/); assert.match(reviewAction, /renderIntakeActionContextHtml\(project\.id, defaultAssistantId\)/); assert.match(reviewAction, /sourceJob \? \[\{ name: "sourceJobContext", label: "来源任务", type: "html", html: renderSourceJobContextHtml\(sourceJob\) \}\] : \[\]/); + assert.match(reviewAction, /value: existingReview\?\.title \|\| defaults\.title \|\| recommendDerivativeJobTitle\(sourceJob, "复盘", ""\)/); assert.match(reviewAction, /normalizePlatformValue\(existingReview\?\.platform \|\| defaults\.platform \|\| sourceJob\?\.platform \|\| "douyin"\)/); });