From e24ca1b0de4b1858256efaf7a3635b52eea4ef71 Mon Sep 17 00:00:00 2001 From: "Azmat@qq.com" Date: Mon, 31 Aug 2026 16:40:16 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A7=86=E9=A2=91=E5=A4=8D=E5=88=BB=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E6=9B=BF=E6=8D=A2=E5=95=86=E5=93=81=E5=92=8C=E6=9B=BF?= =?UTF-8?q?=E6=8D=A2=E8=A7=92=E8=89=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/backend/apps/ai/test_video_replace.py | 51 +++++- core/backend/apps/ai/video_digest.py | 8 +- core/backend/apps/ai/video_replace.py | 147 ++++++++++++++---- .../apps/projects/test_quick_create.py | 1 + .../product-semantic-video-remix/SKILL.md | 64 ++++---- .../backend/skills/video-shot-digest/SKILL.md | 5 +- core/frontend/src/routes/pipeline.tsx | 6 +- core/frontend/src/routes/video-replace.tsx | 24 ++- core/frontend/src/video-replace-page.css | 18 ++- 9 files changed, 253 insertions(+), 71 deletions(-) diff --git a/core/backend/apps/ai/test_video_replace.py b/core/backend/apps/ai/test_video_replace.py index 98905e3..291f709 100644 --- a/core/backend/apps/ai/test_video_replace.py +++ b/core/backend/apps/ai/test_video_replace.py @@ -22,6 +22,7 @@ from apps.ai.video_replace import ( advance_video_replace, build_character_replace_prompt, build_product_replace_prompt, + rewrite_product_semantic_remix as _real_rewrite_product_semantic_remix, run_replace_digest, submit_video_replace, ) @@ -478,7 +479,11 @@ class SubmitVideoReplaceTests(TestCase): return task def test_product_semantic_remix_uses_facts_not_source_product_actions(self): - from apps.ai.video_replace import build_product_semantic_remix_messages, validate_product_semantic_remix + from apps.ai.video_replace import ( + build_product_semantic_remix_messages, + build_safe_product_showcase_prompt, + validate_product_semantic_remix, + ) messages = build_product_semantic_remix_messages( digest_text=DIGEST_SAMPLE, @@ -489,10 +494,42 @@ class SubmitVideoReplaceTests(TestCase): request = messages[1]["content"] self.assertIn("蓝牙耳机", request) self.assertIn("旧牌精华", request) # 参考动作仅作为要剥离的输入 - self.assertIn("商品图片只用于外观", messages[0]["content"]) + self.assertIn("目标商品的外观、颜色、材质、形状", messages[0]["content"]) + self.assertIn("安全展示兜底", request) + self.assertIn("切换顺序和剪辑点必须与拆解稿完全一致", request) + self.assertIn("绝不能改成旁白", request) + self.assertIn("不得说出新商品名称", request) + self.assertIn("不要机械复刻原商品交互", request) + self.assertIn("保留原口播的句数、时间位置、语速节奏和情绪", request) self.assertEqual(validate_product_semantic_remix(SEMANTIC_SAMPLE), SEMANTIC_SAMPLE.strip()) - with self.assertRaisesMessage(ValueError, "商品资料不足"): - validate_product_semantic_remix("MISSING_PRODUCT_FACTS:缺少商品品类") + compiled = validate_product_semantic_remix( + "原商品依赖分析(内部结果)\n【SEEDANCE_PROMPT】\n" + SEMANTIC_SAMPLE + ) + self.assertEqual(compiled, SEMANTIC_SAMPLE.strip()) + self.assertEqual(validate_product_semantic_remix("MISSING_PRODUCT_FACTS:缺少商品品类"), "") + fallback = build_safe_product_showcase_prompt(digest_text=DIGEST_SAMPLE, product_name="蓝牙耳机") + self.assertIn("@目标商品", fallback) + self.assertNotIn("蓝牙耳机", fallback) + self.assertIn("画内人物对白", fallback) + self.assertNotIn("旧牌精华", fallback) + self.assertNotIn("倒进玻璃杯", fallback) + + def test_product_semantic_remix_falls_back_to_safe_showcase_on_missing_facts(self): + """当 LLM 返回 MISSING_PRODUCT_FACTS 时,不抛错,而是平滑兜底为安全展示分镜。""" + text_model = ModelConfig.objects.filter(capability="text", status="active").first() + with patch("apps.ai.video_digest.resolve_digest_model_config", return_value=text_model), \ + patch("apps.ai.services._collect_extract_text", return_value=("MISSING_PRODUCT_FACTS: 缺少商品品类", {})): + prompt = _real_rewrite_product_semantic_remix( + task=None, + digest_text=DIGEST_SAMPLE, + product_facts={"商品名称": "随手拍水杯", "资料状态": "不足"}, + duration=8, + aspect_ratio="9:16", + ) + self.assertIn("@目标商品", prompt) + self.assertNotIn("随手拍水杯", prompt) + self.assertIn("只按参考图展示外观、包装和材质", prompt) + self.assertNotIn("旧牌精华", prompt) def test_product_triview_is_sent_with_the_video(self): """商品库选商品时,三视图要跟着一起发给火山,并在提示词里被点名。""" @@ -587,8 +624,10 @@ class SubmitVideoReplaceTests(TestCase): self.assertIn("@目标角色", prompt) self.assertIn("@参考视频", prompt) self.assertIn("商品", prompt) - self.assertIn("绝不继承参考视频中任何人的原始声线", prompt) - self.assertIn("目标角色为男性时使用自然成年男声", prompt) + self.assertIn("声音重建规则——最高优先级", prompt) + self.assertIn("被替换人物对应的所有声音必须同步替换", prompt) + self.assertIn("不得出现原人物声线残留", prompt) + self.assertIn("嘴部动作必须与重新生成的人声一致", prompt) self.assertNotIn("@目标商品", prompt) def test_digest_generates_once_without_source_video(self): diff --git a/core/backend/apps/ai/video_digest.py b/core/backend/apps/ai/video_digest.py index 27e888a..ad555c6 100644 --- a/core/backend/apps/ai/video_digest.py +++ b/core/backend/apps/ai/video_digest.py @@ -109,7 +109,7 @@ def load_digest_skill() -> str: "你是分镜拆解 agent。输入是一条短视频的完整文件,含画面和音轨。" "必须覆盖全片,从 00:00 写到片尾,有几镜写几镜,不要概括成几大段。" "每镜按【镜头 01】写出时间、时长、景别、机位、运镜、画面、人物动作、人物表情、" - "台词/旁白、音效、背景音乐、字幕、备注。没有就写无。输出中文纯文本。" + "人声方式、台词/旁白、音效、背景音乐、字幕、备注。人声方式只能写画内人物对白、画外旁白或无;没有就写无。输出中文纯文本。" ) @@ -435,15 +435,15 @@ def build_digest_messages( head = [ f"这是一条{measured}的短视频**完整文件**(含画面和口播音轨)。{title_hint}", "请按技能还原全片导演分镜稿:从 00:00 写到片尾,有几镜写几镜。", - "每镜必须写齐时间、时长、景别、机位、运镜、画面、人物动作、人物表情、台词/旁白、音效、背景音乐、字幕、备注。", - "台词/旁白按听到的口播逐字写;画面上的花字写进字幕。", + "每镜必须写齐时间、时长、景别、机位、运镜、画面、人物动作、人物表情、人声方式、台词/旁白、音效、背景音乐、字幕、备注。", + "人声方式必须区分画内人物对白、画外旁白或无;台词/旁白按听到的口播逐字写,画面中人物开口时必须标画内人物对白;画面上的花字写进字幕。", ] else: head = [ f"这是一条{measured}的短视频,", f"按时间顺序均匀抽了 {len(frames)} 帧。每帧图前面标了它在原片中的时间点。{title_hint}", "请按技能还原**全片**导演分镜稿:从 00:00 写到片尾,有几镜写几镜。", - "每镜必须写齐时间、时长、景别、机位、运镜、画面、人物动作、人物表情、台词/旁白、音效、背景音乐、字幕、备注。", + "每镜必须写齐时间、时长、景别、机位、运镜、画面、人物动作、人物表情、人声方式、台词/旁白、音效、背景音乐、字幕、备注。", ] if product_hint: head.append(f"用户接下来想用这条片子的结构去拍自己的商品:{product_hint}。") diff --git a/core/backend/apps/ai/video_replace.py b/core/backend/apps/ai/video_replace.py index ccd97a8..3bf8dfc 100644 --- a/core/backend/apps/ai/video_replace.py +++ b/core/backend/apps/ai/video_replace.py @@ -86,11 +86,21 @@ PRODUCT_PROMPT = ( "严格遵守每镜的新商品动作、场景与口播;不要恢复参考片原商品、原动作、原卖点或原台词。" ) CHARACTER_PROMPT = ( - "以@参考视频的镜头、人物动作、场景、节奏、口播和剪辑为唯一基准生成视频。" - "将画面中所有可见的原人物替换为@目标角色;五官、发型、体态和服装以角色参考图为准。" - "保留原商品、场景、镜头运动、动作顺序、语速、停顿和情绪节奏,不添加字幕、花字或额外人物。" - "绝不继承参考视频中任何人的原始声线或人声:所有对白、旁白、口型和声音都必须与@目标角色的性别与身份一致," - "目标角色为男性时使用自然成年男声,目标角色为女性时使用自然成年女声。" + "@参考视频仅用于参考镜头构图、镜头运动、人物动作、动作顺序、场景、商品、剪辑结构、画面节奏," + "以及原台词表达的文字语义和时间结构。" + "将@参考视频中的原人物替换为@目标角色。目标人物的五官、发型、体态、年龄感、服装和整体形象," + "以@目标角色为唯一基准。" + "保留原视频中的商品、场景、人物动作、镜头运动、动作顺序、剪辑点、画面节奏和故事表达," + "不添加字幕、花字、额外人物或无关元素。" + "【声音重建规则——最高优先级】被替换人物对应的所有声音必须同步替换,包括画面内对白、口播、旁白、画外音、" + "语气词和呼吸声。只保留原口播的文字内容、信息含义、句子顺序、出现时间、停顿位置、情绪类型和表达节奏;" + "不得保留原人物的声线、音色、音高、年龄感、性别特征、口音、声纹或其他说话人身份特征。" + "根据@目标角色的身份及系统提供的目标声音设定,重新生成全部相关人声。" + "从视频开始到结束,被替换人物的声音必须始终使用同一个目标声音;不得出现原人物声线残留、声音身份中途变化、" + "新旧声音混用,或画面角色已经替换但声音仍属于原人物的情况。" + "画面中目标角色的嘴部动作必须与重新生成的人声一致,口型、台词开始时间、停顿、语速和情绪表达保持同步。" + "与人物身份无关的背景音乐、环境音和必要音效可以保留,但其中不得混入原人物的人声或声纹。" + "如保留原视频声音与目标角色声音冲突,必须舍弃原人物声音,以重新生成的目标声音为最高优先级。" ) # 拆解期间的占位提示词。真正的提示词在 worker 拆完后回写。 @@ -380,7 +390,10 @@ def _product_semantic_facts(product: Product) -> dict: "真实卖点": selling_points, "目标用户": product.target_audience.strip(), "规格": product.specs or {}, + "允许使用场景": (product.specs or {}).get("allowed_use_cases") or (product.specs or {}).get("使用场景") or [], + "禁止宣称": (product.specs or {}).get("prohibited_claims") or (product.specs or {}).get("禁止宣称") or [], "资料状态": "完整" if (product.category.strip() and (product.description.strip() or selling_points)) else "不足", + "资料不足时策略": "安全展示兜底:只允许拿取、手持展示、放置、携带、包装/材质/外观特写和在桌面陈列;禁止打开、食用、佩戴、插入、涂抹、连接、充电、清洁、宣称功效或任何未确认用途。", } @@ -398,22 +411,87 @@ def build_product_semantic_remix_messages(*, digest_text: str, product_facts: di f"{facts}\n\n" "【参考视频拆解稿,仅可继承角色关系、环境、镜头语言、剪辑节奏和叙事功能】\n" f"{_digest_for_seedance(digest_text)}\n\n" - "输出给视频模型的中文分镜稿:保留【镜头 01】格式和每镜时间、景别、机位、运镜;" + "保留参考片的人物、场景、拍摄视角、镜头构图、运镜、剪辑顺序、节奏、情绪和营销结构;将原商品完整替换为目标商品。" + "先在内部完成原商品生态、依赖道具、使用接口、动作链与逐镜兼容性检查;不要机械复刻原商品交互。" + "原镜用于取出、展示、使用或证明卖点时,保留该镜景别、运镜、时长和表达目的," + "但仅在动作适合目标商品时保留,否则按目标商品结构、用途和卖点重新设计合理动作。" + "所有持握、开启、穿戴、使用、安装和展示必须符合真实物理常识:不得尺寸不匹配、虚构配件、商品变形换款变色," + "也不得生成未经商品信息支持的功能。" + "如果商品资料状态为‘不足’,不要输出 NEEDS_PRODUCT_FACTS:改用商品资料中的安全展示兜底," + "把原片所有商品使用镜改写为不涉及具体用途的展示镜头。" + "镜头数量、编号、时间区间、时长、景别、机位、运镜、切换顺序和剪辑点必须与拆解稿完全一致,绝不能增删、合并或调换镜头。" + "参考镜为画内人物对白时,成片必须由画面内人物直接开口说话并口型同步,绝不能改成旁白。" + "任何人声都不得说出新商品名称、品牌或旧商品名称,统一用‘这个’、‘它’等自然指代。" + "必须同步重写与原商品有关的口播、音效和画面内容;保留原口播的句数、时间位置、语速节奏和情绪," + "但不得保留原商品的名称、功能和卖点。" + "最后只输出【SEEDANCE_PROMPT】后的中文分镜稿:保留【镜头 01】格式和每镜时间、时长、景别、机位、运镜、人声方式;" "每镜的画面、人物动作、台词/旁白必须按新商品真实用途重写。" - "不要输出分析过程、分类标签或 Markdown。" + "禁止输出分析过程、分类标签或 Markdown。" ), }, ] def validate_product_semantic_remix(text: str) -> str: - """拒绝模型在事实不足时瞎编,或只给一段泛泛分析而没有可出片分镜。""" + """取出模型的最终分镜稿;任何不可用结果由调用方自动降级,绝不阻断用户任务。""" cleaned = (text or "").strip() - if "MISSING_PRODUCT_FACTS" in cleaned: - raise ValueError("商品资料不足:请先在商品库补充品类,以及商品描述或至少一条真实卖点后再复刻") - if len(cleaned) < 120 or "【镜头" not in cleaned: - raise ValueError("商品语义重构结果不完整,请重试") - return cleaned + # skill 的结构化输出只把最终可出片部分送到 Seedance,避免把原商品分析带进提示词。 + marker = "【SEEDANCE_PROMPT】" + if marker in cleaned: + cleaned = cleaned.split(marker, 1)[1].strip() + # 这些是模型的“索取资料”占位语,不能作为视频提示词发送;返回空串使上层直接走展示兜底。 + if any(token in cleaned for token in ("MISSING_PRODUCT_FACTS", "NEEDS_PRODUCT_FACTS", "MANUAL_REVIEW_REQUIRED")): + return "" + return cleaned if len(cleaned) >= 120 and "【镜头" in cleaned else "" + + +def build_safe_product_showcase_prompt(*, digest_text: str, product_name: str) -> str: + """资料不足时的确定性兜底:不依赖模型猜用途,也绝不沿用原商品动作。""" + from .services import enforce_no_embedded_captions + + header, shots = _parse_digest(digest_text) + safe_actions = ( + "人物从干净桌面拿起@目标商品,保持自然手持展示。", + "人物将@目标商品平稳放在桌面上,镜头展示包装轮廓。", + "人物缓慢转动@目标商品,展示外观、包装和材质细节。", + "镜头靠近@目标商品,拍摄包装与可见结构特写。", + ) + safe_dialogues = ( + "你看这个,整体的外观和质感都很清楚。", + "我把它转过来,细节可以看得更完整。", + "从这个角度看,包装和轮廓很直观。", + "镜头靠近一点,看看它的材质细节。", + ) + lines = [ + "【成片规则】", + "@目标商品只按参考图展示外观、包装和材质。", + "不得出现原商品、原容器、原配件、原动作、原台词、功效或任何未经确认的使用方式。", + "镜头数量、时间、时长、景别、机位、运镜和切换顺序必须与参考分镜一致;不得增删、合并、拆分或调换镜头。", + "所有人声禁止说出商品名称和品牌。参考镜中人物开口说话时,必须生成画内人物对白和同步口型,禁止改成旁白。", + ] + if header.get("整体风格"): + lines.append(f"整体风格:{header['整体风格']}") + for index, shot in enumerate(shots, start=1): + lines.extend([ + "", + f"【镜头 {index:02d}】", + *(f"{field}:{shot[field]}" for field in ("时间", "时长", "景别", "机位", "运镜") if shot.get(field)), + f"画面:保持原镜的环境氛围、人物关系与镜头节奏,只出现@目标商品作为展示主体。", + f"人物动作:{safe_actions[(index - 1) % len(safe_actions)]}", + "人物表情:自然、专注。", + f"人声方式:{'画内人物对白' if shot.get('台词/旁白') and not _is_empty_value(shot.get('台词/旁白') or '') else '无'}", + f"台词/旁白:{'画内人物对白(人物对镜开口说话,非旁白):' + safe_dialogues[(index - 1) % len(safe_dialogues)] if shot.get('台词/旁白') and not _is_empty_value(shot.get('台词/旁白') or '') else '无'}", + ]) + if not shots: + lines.extend([ + "", "【镜头 01】", "时长:5秒", "景别:中近景", "机位:平视", "运镜:缓慢推近", + "画面:干净自然的环境中,@目标商品置于桌面中央。", + "人物动作:人物自然手持展示@目标商品。", + "人物表情:自然、专注。", + "人声方式:画内人物对白。", + "台词/旁白:画内人物对白(人物对镜开口说话,非旁白):你看这个,外观和包装细节很清楚。", + ]) + return enforce_no_embedded_captions("\n".join(lines)) def rewrite_product_semantic_remix(*, task, digest_text: str, product_facts: dict, duration: int, aspect_ratio: str) -> str: @@ -421,23 +499,40 @@ def rewrite_product_semantic_remix(*, task, digest_text: str, product_facts: dic from .video_digest import DIGEST_MAX_TOKENS, resolve_digest_model_config from .services import _collect_extract_text, get_text_provider + product_name = str(product_facts.get("商品名称") or "") model_config = resolve_digest_model_config() if model_config is None: - raise ValueError("商品语义重构模型未配置,请联系管理员") - provider = get_text_provider(model_config) - text, _payload = _collect_extract_text( - provider, - model_config, - build_product_semantic_remix_messages( + logger.warning("商品语义重构模型未配置,回退至安全展示分镜") + return build_safe_product_showcase_prompt( digest_text=digest_text, - product_facts=product_facts, - duration=duration, - aspect_ratio=aspect_ratio, - ), - temperature=0.2, - extra_body={"max_tokens": DIGEST_MAX_TOKENS}, + product_name=product_name, + ) + provider = get_text_provider(model_config) + messages = build_product_semantic_remix_messages( + digest_text=digest_text, + product_facts=product_facts, + duration=duration, + aspect_ratio=aspect_ratio, + ) + last_error: Exception | None = None + try: + text, _payload = _collect_extract_text( + provider, model_config, messages, temperature=0.2, + extra_body={"max_tokens": DIGEST_MAX_TOKENS}, + ) + prompt = validate_product_semantic_remix(text) + if prompt: + return prompt + last_error = ValueError("模型未提供可用分镜") + except Exception as exc: + logger.warning("商品语义重构模型调用异常,直接使用展示分镜: %s", exc) + last_error = exc + # 不再因资料、动作兼容或模型格式而拒绝任务:始终继续走 Seedance。 + logger.info("商品语义重构未产出可用分镜,自动使用展示分镜 (原因: %s)", last_error) + return build_safe_product_showcase_prompt( + digest_text=digest_text, + product_name=product_name, ) - return validate_product_semantic_remix(text) def build_character_replace_prompt( diff --git a/core/backend/apps/projects/test_quick_create.py b/core/backend/apps/projects/test_quick_create.py index 7be479c..304ea82 100644 --- a/core/backend/apps/projects/test_quick_create.py +++ b/core/backend/apps/projects/test_quick_create.py @@ -1329,3 +1329,4 @@ class QuickCreateCoordinatorTests(TestCase): self.assertEqual(submit_video.call_count, 2) statuses = list(self.project.video_segments.order_by("sort_order").values_list("status", flat=True)) self.assertEqual(statuses, [VideoSegment.Status.QUEUED, VideoSegment.Status.QUEUED]) + diff --git a/core/backend/skills/product-semantic-video-remix/SKILL.md b/core/backend/skills/product-semantic-video-remix/SKILL.md index 65eb926..bceb2b5 100644 --- a/core/backend/skills/product-semantic-video-remix/SKILL.md +++ b/core/backend/skills/product-semantic-video-remix/SKILL.md @@ -1,46 +1,58 @@ --- name: product-semantic-video-remix -description: 商品语义重构式视频复刻。保留参考片的表达框架,按新商品真实资料重写可执行的视频分镜。 +description: 跨品类商品替换的语义重构规则。参考片只提供视频表达框架,新商品事实决定动作、道具、台词与结果。 --- # 商品语义重构式视频复刻 -## 目标 +## 核心边界 -参考视频仅提供角色关系、环境氛围、镜头语言、剪辑节奏和叙事结构。必须移除原商品的名称、功能、动作、卖点、台词和因果关系,再依据新商品的真实资料重写每个镜头。 +保留参考视频中的人物、场景、拍摄视角、镜头构图、运镜、剪辑顺序、节奏、情绪和营销结构;将原商品完整替换为目标商品。 -## 最高优先级规则 +参考视频只能提供角色关系、兼容环境、光线、景别、机位、运镜、剪辑节奏、情绪曲线、每镜叙事功能,以及人声出现的时间与方式。禁止继承原商品、包装、容器、配件、使用接口、动作、状态变化、作用对象、结果、卖点、品牌和台词。 -1. 复刻镜头意图,不复刻原商品动作。 -2. 新商品真实用途优先于原片视觉相似度。 -3. 商品图片只用于外观、包装、材质和颜色,不得据此编造功效、价格或营销承诺。 -4. 每个动作都必须符合新商品的真实物理用途;原环境冲突时允许最小幅度调整场景。 -5. 信息不足时只输出 `MISSING_PRODUCT_FACTS`,并列出缺少的品类、用途、真实卖点或使用场景;不得猜测后继续写分镜。 +镜头骨架是硬约束:镜头数量、连续编号、每镜时间区间、时长、景别、机位、运镜、切换顺序和剪辑点必须与参考拆解稿逐项一致;不得合并、拆分、删减、增加或调换镜头。只允许重写与原商品绑定的画面内容、动作和人声文字。 -## 工作方法 +目标商品的外观、颜色、材质、形状、尺寸比例、包装和组件以商品图片为准;商品类别、功能、卖点和使用方式以商品信息为准。商品名称和品牌仅用于内部识别,禁止出现在任何画内对白、口播、旁白或画面文字中。 -先从参考分镜中分离四类信息: +## 内部工作步骤 -- PRESERVE:角色数量与关系、环境气氛、镜头时长、景别、机位、运镜、剪辑节奏。 -- ADAPT:每镜叙事功能,例如痛点、展示、验证、收尾。 -- DISCARD:原商品、原包装、原功能、原动作、原台词、原卖点、原结果和绑定道具。 -- VALIDATE:原场景与新商品用途是否冲突;冲突时做最小场景调整。 +1. 建立原商品依赖图:主体、容器、配件、使用接口、动作、状态、结果和专属道具。 +2. 提取可迁移视频 DNA:角色、兼容环境、镜头语言、节奏、情绪曲线及每镜的 Hook / 展示 / 证明 / CTA 功能。 +3. 建立新商品能力模型:形态、部件、可用容器、允许动作、使用者、接触位置、真实卖点、禁止动作与禁止宣称。 +4. 逐镜判定:不要机械复刻原商品交互。原镜用于取出、展示、使用或证明卖点时,保留该镜的景别、运镜、时长和表达目的;只有动作适合目标商品才保留,不适合则按目标商品的结构、用途和卖点重写合理动作。原片的形态、容器、动作、接口、状态和结果只要与新商品不兼容,就必须删除;环境仅做最小必要调整。 +5. 同步重写:所有与原商品有关的口播、音效和画面内容必须改为准确描述目标商品。保留原视频口播的句数、时间位置、语速节奏和情绪,不得保留错误的商品名称、功能或卖点。 +6. 逐镜校验:人物与商品的持握、开启、穿戴、使用、安装和展示必须符合真实物理常识;商品不得从尺寸不匹配的容器中取出,不得出现不存在的配件,不得变形、换款、变色或产生未经商品信息支持的功能。动作几何、容器尺度、状态前后关系、商品外观、持有关系、卖点事实和跨镜连续性全部成立才能出片。 -然后只根据输入的商品名称、品类、描述、真实卖点、目标用户和规格,建立新商品的能力边界:它是什么、能做什么、不能做什么、谁在何处如何使用。不得把参考片的护肤、饮用、穿戴、清洁、数码操作等动作直接迁移到不同品类。 +不要把“把 A 换成 B,其他不变”当成替换。最低替换单位是:商品主体 → 部件/容器/配件 → 使用接口 → 动作 → 状态变化 → 可见结果。 -## 输出要求 +## 信息不足与失败 -只输出中文导演分镜稿,不输出分析、分类标签、解释或 Markdown。必须保留: +没有品类,或无法确认用途/真实卖点时,优先使用“安全展示兜底”继续:只可写拿取、手持展示、放置、携带、包装/材质/外观特写和桌面陈列;禁止打开、食用、佩戴、插入、涂抹、连接、充电、清洁、功效、结果和营销宣称。把原片所有商品使用镜改写为这些安全展示镜头,不得猜测具体用途。 -- `【镜头 01】` 连续编号; -- 每镜的时间、时长、景别、机位、运镜; -- 依据新商品事实重写后的画面、人物动作、人物表情、台词/旁白; -- 角色关系、环境连续性和原片节奏。 +即使资料不足或动作难以兼容,也必须继续输出安全展示分镜;不得输出索取资料、人工审核或拒绝生成的占位文字。 -开头必须有 `【成片规则】`,明确:参考视频不决定新商品发生什么;新商品外观以 `@目标商品` 参考图为准;禁止出现原商品、原品牌、原包装、原功能和原台词。 +## 最终输出 -台词只能使用已提供的商品事实和真实卖点。没有可证实的功效时,改写为可见的使用、外观、携带、摆放或操作事实,不得虚构前后效果。 +在内部完成上述分析与校验,但不要输出分析过程。仅输出以下内容: -## 逐镜校验 +``` +【SEEDANCE_PROMPT】 +【成片规则】 +参考视频拆解结果仅用于角色关系、可兼容环境、镜头语言、剪辑节奏、情绪曲线和叙事结构。禁止继承原商品及其容器、配件、使用动作、作用对象、状态变化、功能结果、商品台词和专属道具。 +@目标商品:新商品外观、包装和材质参考。 -生成前逐镜确认:新商品能否真的执行该动作、使用者与场景是否合理、动作是否证明真实卖点、前后是否连续、是否残留原商品语义、是否有未经提供依据的功效,以及商品是否保持与参考图一致。任一项不满足时,先重写该镜。 +【镜头 01】 +时间:... +时长:... +景别:... +机位:... +运镜:... +画面:...(新商品自己的合理道具、位置和状态) +人物动作:...(只使用允许动作) +人物表情:... +人声方式:画内人物对白 / 画外旁白 / 无(必须沿用参考片该镜的人声方式) +台词/旁白:...(只使用已确认事实;画内人物对白必须让画面中的角色开口说话、口型同步,禁止改成旁白;不得说商品名称或品牌) +``` + +镜头从 `【镜头 01】` 连续编号。必须保留原片的时间、时长、景别、机位、运镜、切换点、叙事功能和人声方式;重写画面、动作、台词与结果。禁止字幕、花字、原商品、原品牌、商品名称和未经证实的功效。 diff --git a/core/backend/skills/video-shot-digest/SKILL.md b/core/backend/skills/video-shot-digest/SKILL.md index 292f4d2..7da2471 100644 --- a/core/backend/skills/video-shot-digest/SKILL.md +++ b/core/backend/skills/video-shot-digest/SKILL.md @@ -32,6 +32,7 @@ description: > ### 铁律 2 · 声音按听到的写 你拿得到音轨。 +- `人声方式`:必须标明「画内人物对白」(看得到人物开口说话)、「画外旁白」(说话者不在画面内)或「无」。不可把画内人物对白笼统写成旁白。 - `台词/旁白`:口播或对白**原文**,逐字转写,不要润色、不要概括。 - `字幕`:画面上的花字 / 字幕**逐字照抄**;没有就写「无」。 - `音效`:雨声、脚步、开瓶、汽车经过等你确实听见的同期声。 @@ -67,6 +68,7 @@ description: > 画面:深夜街道被雨水覆盖,路灯倒映在积水中,一名黑衣女孩独自走过空旷街口。 人物动作:女孩低头行走,右手撑着黑伞。没有人物写「无」。 人物表情:警觉、疑惑。看不见脸写「不可见」。 +人声方式:画内人物对白。 台词/旁白:是谁? 没有人声写「无」。 音效:雨声、远处汽车经过声。 背景音乐:低沉而克制的钢琴音。 @@ -82,6 +84,7 @@ description: > 画面:镜头跟随女孩前进,她的外套和头发被风吹动。 人物动作:女孩突然放慢脚步,微微转头。 人物表情:警觉、疑惑。 +人声方式:画内人物对白。 台词/旁白:是谁? 音效:脚步声停止,雨声短暂增强。 背景音乐:钢琴停止,加入低频氛围音。 @@ -98,7 +101,7 @@ description: > - 镜号写成 `【镜头 01】` 这种两位数字,从 01 连续递增。 - `时间` 用 `00:00-00:04` 这种分:秒闭区间,必须接上上一镜结束点,不能跳秒、不能重叠。 - `时长` 必须等于该镜时间区间的秒数。 -- 上面列出的栏位**每一镜都要有**,不要省略;没有内容就写「无」或「不可见」。 +- 上面列出的栏位**每一镜都要有**,不要省略;没有内容就写「无」或「不可见」。`人声方式` 只能是「画内人物对白 / 画外旁白 / 无」。 - **`【拆解存疑】`一节必须有**,哪怕只有一条。 - 全文中文,不出现 markdown 标题符号(`#`)和代码块围栏。 - 不要把一镜压成「画面 + 解说词」两行。栏位太少,下游脚本就只能得到一句摘要。 diff --git a/core/frontend/src/routes/pipeline.tsx b/core/frontend/src/routes/pipeline.tsx index 7f2d552..2905cf1 100644 --- a/core/frontend/src/routes/pipeline.tsx +++ b/core/frontend/src/routes/pipeline.tsx @@ -37,6 +37,8 @@ const KIND_LABEL: Record = { product: "商品", person: "角色" const SOURCE_LABEL: Record = { ai: "脚本辅助生成", theme: "脚本辅助生成", manual: "上传脚本", video: "上传视频提炼" }; // 上传自有脚本 / 上传视频提炼入口暂隐,改 true 即可恢复 const SHOW_SCRIPT_IMPORT = false; +// 脚本模型选择入口暂隐(目前仅一个模型),改 true 或多模型时可恢复 +const SHOW_SCRIPT_MODEL_PICKER = false; // 旁白配音音色预设(语音合成经典版试用包实测可用,与后端 VOICEOVER_VOICES 对齐) const VO_VOICES = [ { key: "BV700_streaming", label: "灿灿 · 活力女声" }, @@ -3350,8 +3352,8 @@ export function PipelinePage(props: { )} - {/* 模型选择小按钮(输入框下方,对齐 ChatGPT/Lovart)· 复用 restraint chip 下拉,向上展开 */} - {scriptPickerModels.length > 0 ? ( + {/* 模型选择小按钮(输入框下方,对齐 ChatGPT/Lovart)· 目前单一模型先隐藏 */} + {SHOW_SCRIPT_MODEL_PICKER && scriptPickerModels.length > 1 ? (
tempInputRef.current?.click()} + tabIndex={generating ? -1 : 0} + aria-disabled={generating} + {...(!generating ? tempDrop.dropProps : {})} + onClick={() => { + if (generating) return; + tempInputRef.current?.click(); + }} onKeyDown={(event) => { + if (generating) return; if (event.key === "Enter" || event.key === " ") { event.preventDefault(); tempInputRef.current?.click(); @@ -1023,9 +1031,11 @@ export function VideoReplacePage({ type="button" className="replace-temporary-remove" aria-label={`删除第${index + 1}张临时${copy.temporaryNoun}`} + disabled={generating} onClick={(event) => { event.preventDefault(); event.stopPropagation(); + if (generating) return; if (tempFiles.length) { setTempFiles((current) => current.filter((_, itemIndex) => itemIndex !== index)); } else { @@ -1049,9 +1059,11 @@ export function VideoReplacePage({