feat: plan storyboard frame endpoints

This commit is contained in:
2026-05-18 09:47:13 +08:00
parent cf648eaac2
commit 75c5d113ee
6 changed files with 316 additions and 39 deletions

View File

@@ -84,6 +84,20 @@ const PRODUCT_FUSION_NEGATIVE_PROMPT = [
"no product passing through the neck, no product inside the transparent body, no x-ray blending, no transparent product, no product becoming bones or skin, no product fused with spine/ribs/throat, no clipping through shoulders, no floating device, no melted device, no deformed U-shape, no wrong body part, no necklace/scarf/headphones/brace, no random replacement product.",
].join("\n")
function storyboardNeedsProduct(scene: StoryboardScene) {
if (scene.needs_product === false) return false
if (scene.needs_product === true) return true
const text = `${scene.visual_mode ?? ""} ${scene.product ?? ""} ${scene.product_placement ?? ""}`.toLowerCase()
return !/(不出现产品|不露产品|无需产品|不需要产品|无产品|no product|environment|person_only)/.test(text)
}
function storyboardNeedsSubject(scene: StoryboardScene) {
if (scene.needs_subject === false) return false
if (scene.needs_subject === true) return true
const text = `${scene.visual_mode ?? ""} ${scene.subject ?? ""}`.toLowerCase()
return !/(不需要人物|无人物|不出现人物|no person|product_only|environment)/.test(text)
}
// 合并 input + download + split 为一个节点
// 分叉:上路 input → visual lab ↘
// 下路 input → audio ──────────────────────────→ compose
@@ -565,8 +579,10 @@ export default function Home() {
: null
const firstRef = scene.first_image ?? keyframeRef
const lastRef = scene.last_image ?? defaultLastRef
let productRefs = (scene.product_images?.length ? scene.product_images : scene.product_image ? [scene.product_image] : [])
if (productRefs.length === 0) {
const needsProduct = storyboardNeedsProduct(scene)
const needsSubject = storyboardNeedsSubject(scene)
let productRefs = needsProduct ? (scene.product_images?.length ? scene.product_images : scene.product_image ? [scene.product_image] : []) : []
if (needsProduct && productRefs.length === 0) {
try {
productRefs = await ensureDefaultProductRefs(job.id)
} catch (e) {
@@ -574,7 +590,7 @@ export default function Home() {
return
}
}
const subjectRefs: ImageRef[] = (frame.elements ?? [])
const subjectRefs: ImageRef[] = needsSubject ? (frame.elements ?? [])
.flatMap((element) => element.subject_assets ?? [])
.slice(0, 6)
.map((asset) => ({
@@ -583,8 +599,8 @@ export default function Home() {
element_id: asset.id,
cutout_id: asset.id,
label: asset.label,
}))
const primarySubjectRef = subjectRefs[0] ?? firstRef
})) : []
const primarySubjectRef = needsSubject ? (subjectRefs[0] ?? firstRef) : null
const duration = scene.duration && scene.duration > 0 ? scene.duration : 5
const sourceScene = frame.description?.scene ? `参考画面识别:${frame.description.scene}` : ""
const sourceStyle = frame.description?.style ? `参考风格:${frame.description.style}` : ""
@@ -607,31 +623,47 @@ export default function Home() {
].join("\n")
const prompt = [
`竖屏 9:16${duration.toFixed(1)}SKG 产品短视频广告。`,
productNature,
productRefs.length
needsProduct
? productNature
: "本条分镜规划为非产品主镜头:可以只拍人物状态、场景过渡、情绪停点或节奏承接。不要硬插 SKG 产品、白底产品图、包装或任何随机商品。",
needsProduct && productRefs.length
? `已上传 ${productRefs.length} 张 SKG 真实产品参考图。产品参考图是唯一产品真源:视频中出现的产品必须严格匹配这些图的外观、颜色、材质、结构比例和关键细节。`
: "未上传产品图时,仍需生成一个干净高级的 SKG 产品广告画面,但不得保留原视频里的竞品包装或平台元素。",
"首帧和尾帧只用于控制画面起止、构图、场景和动作方向;如果首尾帧里有竞品、文字包装或非 SKG 产品,必须替换为上传的 SKG 产品参考。",
: needsProduct
? "未上传产品图时,仍需生成一个干净高级的 SKG 产品广告画面,但不得保留原视频里的竞品包装或平台元素。"
: "本条不传产品参考图;如首尾帧里出现竞品、包装或非 SKG 商品,应弱化、移除或作为模糊背景,不要替换成 SKG 产品。",
needsProduct
? "首帧和尾帧只用于控制画面起止、构图、场景和动作方向;如果首尾帧里有竞品、文字包装或非 SKG 产品,必须替换为上传的 SKG 产品参考。"
: "首帧和尾帧用于控制画面起止、构图、场景和动作方向;本条没有产品任务,不要因为广告语而自动添加产品。",
"使用首帧和尾帧生成连续过渡视频:首帧必须严格作为视频开始画面,尾帧必须作为视频结束目标画面,中间只做自然运动补间。",
"生成一段单镜头连续视频,一镜到底,从首帧平滑过渡到尾帧;不要跳切,不要突然换场景,不要突然换主体,不要蒙太奇,不要多镜头拼接。",
"如果提供了原视频链接,把它只作为节奏、镜头运动、动作顺序和画面调度参考;不要照搬原视频里的品牌、文字、水印、竞品产品或具体人物。",
"时间线0%-15% 锁住首帧构图并轻微启动15%-85% 做平滑连续运动85%-100% 缓慢贴近尾帧并稳定收住。",
TRANSPARENT_HUMAN_VIDEO_PROMPT,
`镜头类型:${scene.visual_mode ?? "未标注"};需要人物=${needsSubject ? "是" : "否"};需要产品=${needsProduct ? "是" : "否"}`,
scene.first_frame_plan ? `首帧规划:${scene.first_frame_plan}` : "",
scene.last_frame_plan ? `尾帧规划:${scene.last_frame_plan}` : "",
scene.product_placement ? `产品出现方式:${scene.product_placement}` : "",
needsSubject
? TRANSPARENT_HUMAN_VIDEO_PROMPT
: "本条不传人物主体参考图;如果画面需要人物,只能作为背景、手部局部或模糊生活方式元素,不要生成主角式透明骨架人。",
`主体改造:${subjectDirection}`,
`产品替换:${productDirection} 产品必须作为颈部/肩颈按摩仪被正确佩戴或展示,不要放在脸上、手臂上、桌面当摆件,也不要变成瓶子、面霜、医疗设备或食品。`,
needsProduct
? `产品替换:${productDirection} 产品必须作为颈部/肩颈按摩仪被正确佩戴或展示,不要放在脸上、手臂上、桌面当摆件,也不要变成瓶子、面霜、医疗设备或食品。`
: `产品处理:${productDirection} 本条不需要露出 SKG 产品,不要硬插产品、包装、瓶罐、医疗器械或随机商品。`,
`场景改造:${sceneDirection}`,
`连续动作和镜头:${actionDirection}`,
`首帧:${labelOf(firstRef, "当前分镜关键帧")}`,
`尾帧:${labelOf(lastRef, "未指定,按首帧小幅自然运动收尾")}`,
`SKG 产品参考:${productRefs.length ? productRefs.map((ref, i) => `${i + 1}. ${labelOf(ref, "SKG 产品角度")}`).join("") : "SKG 产品视觉主角"}`,
subjectRefs.length ? `关键元素 6 视图参考:${subjectRefs.map((ref, i) => `${i + 1}. ${labelOf(ref, "元素视图")}`).join("")}` : "如果该分镜还没有关键元素 6 视图,优先使用首帧主体关系生成。",
needsProduct ? `SKG 产品参考:${productRefs.length ? productRefs.map((ref, i) => `${i + 1}. ${labelOf(ref, "SKG 产品角度")}`).join("") : "SKG 产品视觉主角"}` : "SKG 产品参考:本条不使用产品参考图。",
needsSubject
? (subjectRefs.length ? `关键元素 6 视图参考:${subjectRefs.map((ref, i) => `${i + 1}. ${labelOf(ref, "元素视图")}`).join("")}` : "如果该分镜还没有关键元素 6 视图,优先使用首帧主体关系生成。")
: "关键元素 6 视图参考:本条不使用人物主体参考图。",
sourceScene,
sourceStyle,
sourceObjects,
"产品一致性要求:整个视频只能出现同一个白色 U 形 SKG 颈部按摩仪或同一套包装;不要生成第二种产品,不要改变 U 形机身、金属按摩触点、侧边按键、白色材质和整体比例,不要凭空增加屏幕、线缆、文字标签或说明书。",
"产品呈现要求:至少一次让产品在脖子/肩颈位置清晰占据视觉中心,边缘清楚、材质真实、比例可信;手部接触产品时不要遮挡关键外观,产品不能融化、扭曲、穿帮或漂移。",
"状态改善要求:画面应形成明确的使用前后感受变化:使用前可以是低头久坐、揉脖子、肩颈疲惫或紧绷;使用后变为肩颈放松、抬头、动作舒展、精神更好。人形骷髅也可以表现为从僵硬难受变轻松放松。表达舒缓和放松,不要承诺治疗。",
"运动要求:动作幅度小而连续,速度均匀,手部和产品位置前后一致,产品外形不变形,人物表情和姿态不漂移,背景只允许轻微景深和光影变化。",
needsProduct ? "产品一致性要求:整个视频只能出现同一个白色 U 形 SKG 颈部按摩仪或同一套包装;不要生成第二种产品,不要改变 U 形机身、金属按摩触点、侧边按键、白色材质和整体比例,不要凭空增加屏幕、线缆、文字标签或说明书。" : "",
needsProduct ? "产品呈现要求:至少一次让产品在脖子/肩颈位置清晰占据视觉中心,边缘清楚、材质真实、比例可信;手部接触产品时不要遮挡关键外观,产品不能融化、扭曲、穿帮或漂移。" : "",
needsSubject || needsProduct ? "状态改善要求:画面应形成明确的使用前后感受变化:使用前可以是低头久坐、揉脖子、肩颈疲惫或紧绷;使用后变为肩颈放松、抬头、动作舒展、精神更好。人形骷髅也可以表现为从僵硬难受变轻松放松。表达舒缓和放松,不要承诺治疗。" : "节奏要求:作为过渡镜头时只负责情绪、空间和节奏承接,不承诺疗效,不强行展示使用动作。",
needsProduct ? "运动要求:动作幅度小而连续,速度均匀,手部和产品位置前后一致,产品外形不变形,人物表情和姿态不漂移,背景只允许轻微景深和光影变化。" : "运动要求:动作幅度小而连续,速度均匀,构图从首帧自然过渡到尾帧,不突然添加人物或产品。",
"商业质感:真实拍摄感,干净高级,柔和稳定打光,产品边缘清晰,材质真实,画面无抖动、无拉伸、无闪烁。",
"禁止:字幕、文字、平台 UI、TikTok 水印、logo 水印、免责声明、竞品包装、随机新物体、非 SKG 产品、医学骨架、夸张病症画面、恐怖元素、画面撕裂、人物或产品突然变形。",
TRANSPARENT_HUMAN_NEGATIVE_PROMPT,
@@ -649,7 +681,7 @@ export default function Home() {
subject_image: primarySubjectRef,
subject_images: subjectRefs,
scene_image: null,
product_image: productRefs[0] ?? null,
product_image: needsProduct ? (productRefs[0] ?? null) : null,
action_image: null,
source_ref: sourceUrl ? { kind: "source_video", url: sourceUrl } : null,
model,