diff --git a/core/backend/_seed_seg7.py b/core/backend/_seed_seg7.py index e7e9f95..7e52eb2 100644 --- a/core/backend/_seed_seg7.py +++ b/core/backend/_seed_seg7.py @@ -56,7 +56,7 @@ if made: (0, 5, "钩子", "未出现", "连着熬三天,这张脸还有救吗?"), (1, 8, "痛点", "背景陈列", "早上照镜子,毛孔粗、脸发黄,粉底都盖不住。"), (2, 12, "卖点", "手持展示", "这盒夜光面膜敷 10 分钟,精华浓度是普通款的三倍。"), - (3, 5, "CTA", "特写", "点下方小黄车,今晚下单买二送一。"), + (3, 5, "CTA", "特写", "家里常煮茶的,这盒真省事,今晚买二送一。"), ]: ScriptSegment.objects.create( script_version=script, diff --git a/core/backend/apps/ai/free_video.py b/core/backend/apps/ai/free_video.py index 1dc4f87..eb393f4 100644 --- a/core/backend/apps/ai/free_video.py +++ b/core/backend/apps/ai/free_video.py @@ -620,8 +620,9 @@ def start_pending_free_video(task: AITask) -> AITask: except (TypeError, ValueError): seed = -1 references = list(payload.get("references") or []) - if feature == "video_replace": - # 复刻:用户原片只用来提炼,绝不能进 Seedance,否则真人素材直接被火山拒。 + if feature == "video_replace" and payload.get("replace_mode") == "product": + # 商品复刻:原片只用来提炼,绝不能进 Seedance,否则真人素材直接被火山拒。 + # 角色复刻走另一条已入火山素材库的 video-reference 链路,必须保留视频。 references = [item for item in references if (item or {}).get("type") != "video"] try: built = build_content_items( diff --git a/core/backend/apps/ai/generation_errors.py b/core/backend/apps/ai/generation_errors.py index fb7d349..379745e 100644 --- a/core/backend/apps/ai/generation_errors.py +++ b/core/backend/apps/ai/generation_errors.py @@ -415,6 +415,8 @@ def classify_generation_error( "画面描写太短", "按秒拆分镜", "切两次景别", + "机位与运镜", + "平台指令腔", ): return _build_error("processing_failed", operation, reference_id=reference_id) return _build_error("unknown", operation, reference_id=reference_id) diff --git a/core/backend/apps/ai/script_agent.py b/core/backend/apps/ai/script_agent.py index ea8a71f..e3d4d4a 100644 --- a/core/backend/apps/ai/script_agent.py +++ b/core/backend/apps/ai/script_agent.py @@ -494,7 +494,11 @@ _CREATIVE_DIRECTION = """ - 痛点镜:拍得到的细节,而非抽象感受。例如「开会前刘海粘成一绺」而不是「头发很油」。 - 卖点镜:只证明一个卖点,必须写清「原本卡在哪里 → 手怎么使用商品 → 眼前有什么变化」。 不准只报参数、堆形容词或重复商品名。 -- CTA 镜:回到前面那个具体情境,给一个自然的选择/动作;不要硬喊「赶紧下单」。 +- CTA 镜:回到前面那个具体情境,说清**谁适合、为什么值得试**,用一句像跟朋友说话的口语收尾。 + **禁止平台指令腔**:小黄车、购物车、点下方、点击购买、立即购买、马上/赶紧下单、抢购、秒杀、 + 手慢无、别犹豫、买它、闭眼入、上车 —— 这些一出现整条就掉回广告,观众直接划走。 + 合格例子:「你要是也天天下午三点犯困,可以试试这个」「链接放这儿了,先看看合不合适」。 + 紧迫感只能来自商品资料里的真实事实,没有就不制造;画面里也不许出现购买浮层或价格贴片。 ### 画面与台词必须互相提供新信息 - 每个 15 秒场只安排**一个连续的核心动作链**,例如「拆开 → 倒入 → 颜色变化」, @@ -517,7 +521,7 @@ _CREATIVE_DIRECTION = """ ``` 【本镜任务】这一段要让观众看懂的变化或悬念。 【光线氛围】光源方向与性质(窗光/顶光/台灯/逆光);色温冷暖;明暗对比;整体色调。全片各镜保持一致。 -【声音】台词/旁白状态;音效;背景音乐;字幕。没有就写「无」;没有配乐写「无配乐,仅同期声」。 +【声音】台词/旁白状态;音效;背景音乐。没有就写「无」;没有配乐写「无配乐,仅同期声」。**不要写字幕这一栏** —— 本系统出的视频一律无字幕。 【画面内容】 0-3s:景别;机位高度与角度;运镜;主体在画面中的位置;具体动作;情绪或信息变化。 3-8s:景别;机位;运镜;手与商品的空间关系(哪只手、握哪里、从哪个方向入画);可见细节。 @@ -536,8 +540,9 @@ _CREATIVE_DIRECTION = """ - **禁止抽象词**:「展示商品」「呈现质感」「体现高级感」「氛围到位」「传递温暖」—— 这些不是画面,是评价。要写成「杯壁上的水珠顺着往下滑」「她眉头松开,肩膀塌下来」。 - **禁止心理描写**:模型拍不出「她内心很纠结」,要写「她拿起又放下,手指在包装边缘停了两秒」。 -- **禁止画面里出现文字**:不要写字幕、花字、标题、价格贴片、UI、弹窗、对比图表; - 商品包装上原有的真实文字除外。 +- **禁止画面里出现文字**:本系统出的**任何视频都不带字幕**。不要写字幕、花字、标题、片头片尾、 + 水印、角标、价格贴片、优惠标签、弹幕、对话气泡、贴纸、参数表、对比图表、UI、按钮、购物浮层; + 口播和台词只以声音存在。商品包装上原有的真实文字除外。 - **禁止一镜内跨场景/跨时间蒙太奇**:15 秒是**一个连续的动作链**,同一地点、同一光线、 同一套衣服。要换环境就换到下一镜,并在 entity_refs 里换成另一个 scene。 @@ -546,10 +551,20 @@ _CREATIVE_DIRECTION = """ 包装、光线、地点或色调。每一镜的【光线氛围】要和相邻镜衔接得上(同一场戏不能上一镜暖黄台灯、 下一镜冷白日光)。商品的用法要符合物理常识:热饮有蒸汽、液体会流动、包装要先打开才能取出内容物。 -输入如果是 `【镜头 01】` 导演分镜稿,把原稿的景别、机位、运镜、动作、表情、音效、背景音乐、字幕、备注 -折进对应栏,不要压成一句画面摘要。 +输入如果是 `【镜头 01】` 导演分镜稿,把原稿的景别、机位、运镜、动作、表情、音效、背景音乐、备注 +折进对应栏,不要压成一句画面摘要。**原稿里的「字幕 / 花字」一栏一律丢弃**,绝不搬进新脚本 —— +参考视频有字幕不代表我们的成片要有。 """ +# CTA 平台指令腔:一出现整条就从「真人分享」掉回「电视购物」,且换平台就穿帮。 +# 用户明确要求全系统去掉「小黄车」那套话术,这里做成硬闸,模型跑偏就打回重写。 +_CTA_BANNED_PHRASES = ( + "小黄车", "购物车", "点下方", "点击下方", "点下面", "点击购买", "立即购买", "立刻购买", + "马上下单", "赶紧下单", "立即下单", "抓紧下单", "抢购", "秒杀", "手慢无", "别犹豫", + "买它", "闭眼入", "上车", "冲鸭", +) + + # 这些句式几乎总会让首屏像模板广告。只作为生成后的最后一道门,不替代模型的创作判断。 _GENERIC_HOOK_PHRASES = ( "大家好", "今天", "给大家", "给你们", "给姐妹", "分享一下", "推荐一下", @@ -739,7 +754,8 @@ def build_agent_messages( "并写清哪只手、从哪个方向入画、握商品哪里,以及真实用法" "(茶=热水+蒸汽+茶汤变色,禁止茶包丢进冷白开;禁止悬浮肢体、商品凭空出现)。" "只写拍得出来的东西:禁止「展示商品/呈现质感/体现高级感」这类评价词,禁止心理描写," - "禁止画面里出现字幕花字标题价签,禁止一镜内跨场景跨时间。" + "禁止画面里出现任何文字叠加(字幕/花字/标题/水印/角标/价签/购物浮层),口播只以声音存在;" + "禁止一镜内跨场景跨时间。" f"禁止一句空画面撑满 {SEGMENT_DURATION_MAX} 秒。允许另给 beats 数组,后端会折进 visual。\n" ) if fmt == "oral": @@ -817,8 +833,9 @@ def build_agent_messages( "【任务】参考视频改写:用户给了一份逐镜拆解稿。" "照搬它的镜头顺序、每镜时长比例、景别、机位、运镜、人物动作和声音层次," "把人物、商品、品牌、台词全部换成当前商品。" - "写 visual 时把拆解稿里的景别/机位/运镜/人物动作/表情/音效/背景音乐/字幕/备注" + "写 visual 时把拆解稿里的景别/机位/运镜/人物动作/表情/音效/背景音乐/备注" "折进【声音】和【画面内容】的秒级分镜,不要压成一句画面摘要。" + "**拆解稿里的「字幕」栏一律丢弃**,不要搬进新脚本 —— 我们的成片不带字幕。" "原稿写「无 / 不可见 / 听不清」的栏不要编造。" "钩子、已选结构的核心步骤和商品事实必须能对上。" "每镜旁白要能撑满指定时长;画面必须按秒拆分镜,够导演在 15 秒里切 3–5 刀。\n" @@ -1044,6 +1061,24 @@ def assert_script_has_a_hook(draft: dict) -> None: raise ValueError("开场像泛泛推荐,请直接从具体场景、反差或问题切入") +def assert_cta_is_not_shouty(draft: dict) -> None: + """拦住「点下方小黄车」那套平台指令腔:CTA 要回到情境、点名谁适合,不是喊口号。 + 扫全片口播/对白(不只 CTA 镜),因为这类话经常顺手塞进末镜旁白的最后一句。""" + for index, seg in enumerate(draft.get("segments") or []): + if not isinstance(seg, dict): + continue + speech = _speech_text(seg) + hit = next((phrase for phrase in _CTA_BANNED_PHRASES if phrase in speech), None) + if hit: + raise ValueError( + f"第 {index + 1} 镜出现了平台指令腔「{hit}」,请改成回到具体情境、点名谁适合的自然口语收尾" + ) + hook = str(draft.get("hook") or "") + hit = next((phrase for phrase in _CTA_BANNED_PHRASES if phrase in hook), None) + if hit: + raise ValueError(f"钩子里出现了平台指令腔「{hit}」,请换成具体场景或反差") + + def assert_intra_shot_beats(seg: dict, index: int, duration: int) -> None: """15 秒场必须按秒拆出分镜,否则下游视频只能对着一句空描述乱编。""" need = min_beats_for_duration(duration) @@ -1727,6 +1762,7 @@ def stream_script_agent( selling_titles=selling_titles, ) assert_shot_density(candidate, fmt) + assert_cta_is_not_shouty(candidate) return candidate routed_stream = stream_routed_text_request( diff --git a/core/backend/apps/ai/services.py b/core/backend/apps/ai/services.py index 52abb99..d22b656 100644 --- a/core/backend/apps/ai/services.py +++ b/core/backend/apps/ai/services.py @@ -872,14 +872,42 @@ class VideoSubmissionStateUnknown(ProviderOutcomeUnknownError): """视频提交可能已到达供应商但未拿到可靠任务 ID;禁止自动重提。""" +# 本系统出的任何视频都不带字幕。这条是最高优先级的成片硬性禁令,写得穷尽一点 —— +# 实测只写「不要字幕」时,模型仍会自作主张加花字 / 价格贴片 / 购物浮层。 NO_EMBEDDED_CAPTIONS_REQUIREMENT = ( - "【成片硬性规则】画面不得出现字幕、台词字幕、花字、标题、价格贴片、UI 文字或其他后期文字叠加;" - "只保留商品包装上参考图中原有的真实物理文字与标识。旁白和环境音可以保留,但不要把声音转成画面文字。" + "【字幕硬性禁令 · 最高优先级】本片是**无字幕成片**。画面中绝对不能出现任何后期文字或图形叠加,包括但不限于:" + "字幕、台词字幕、双语字幕、注音、标题、片头、片尾、花字、艺术字、水印、角标、台标、" + "logo 贴片、价格贴片、优惠券、促销标签、倒计时、弹幕、对话气泡、贴纸、表情包文字、" + "参数表、成分表、对比图表、评分条、UI 界面、按钮、购物车 / 购买浮层 / 商品卡、进度条、时间码。" + "口播、台词、旁白、音效只以**声音**存在,绝不转写成画面上的文字。" + "画面里唯一允许出现的文字,是参考图中商品包装 / 标签上原本就有的真实物理文字与品牌标识," + "且必须与参考图完全一致,不得放大、重排或新增。" + "即使本提示词别处提到了「字幕」「花字」「标注」等字样,也一律以本条为准:不生成任何文字叠加。" ) +# 提示词正文里残留的「字幕:…」「花字:…」会和上面的禁令打架 —— 模型看到具体字幕内容就会画出来。 +# 来源:脚本【声音】栏、视频提炼稿逐字照抄的画面花字、用户自己写的提示词。这里在送出前统一抹掉。 +_CAPTION_FIELD_LINE_RE = re.compile(r"^\s*[-•*]?\s*(?:字幕|花字|艺术字|贴片|水印|角标|标题栏)\s*[::].*$") +_CAPTION_FIELD_INLINE_RE = re.compile(r"[,,;;、]?\s*(?:字幕|花字|艺术字|贴片|水印|角标)\s*[::][^;;。\n]*") + + +def strip_caption_directives(prompt: str) -> str: + """抹掉提示词里「字幕:xxx / 花字:xxx」这类会诱使模型画文字的栏位(整行或行内片段)。 + 只删这类字段,不动其余正文;删空的行直接丢掉。""" + out: list[str] = [] + for line in (prompt or "").split("\n"): + if _CAPTION_FIELD_LINE_RE.match(line): + continue + cleaned = _CAPTION_FIELD_INLINE_RE.sub("", line) + if cleaned.strip() or not line.strip(): + out.append(cleaned) + return "\n".join(out) + + def enforce_no_embedded_captions(prompt: str) -> str: - """所有视频入口最终汇入这里,避免某个入口漏传“无字幕”导致模型自行加花字。""" - base = (prompt or "").strip() + """所有视频入口(专业创作 / 一键成片 / 自由创作 / 视频复刻 / 提炼改写)最终汇入这里: + 先洗掉正文里的字幕字段,再追加最高优先级的无字幕禁令。少一个入口都不行。""" + base = strip_caption_directives(prompt or "").strip() if NO_EMBEDDED_CAPTIONS_REQUIREMENT in base: return base return f"{base}\n{NO_EMBEDDED_CAPTIONS_REQUIREMENT}".strip() diff --git a/core/backend/apps/ai/test_script_duration_combo.py b/core/backend/apps/ai/test_script_duration_combo.py index c25f39d..35b961f 100644 --- a/core/backend/apps/ai/test_script_duration_combo.py +++ b/core/backend/apps/ai/test_script_duration_combo.py @@ -23,6 +23,7 @@ from apps.ai.script_agent import ( allowed_structures, assert_product_facts_used, assert_script_has_a_hook, + assert_cta_is_not_shouty, assert_shot_density, build_agent_messages, coerce_combo, @@ -638,3 +639,31 @@ class RecommendScriptSetupTests(SimpleTestCase): self.assertEqual(rec["format"], "oral") self.assertEqual(rec["structure"], "pain") self.assertEqual(rec["persona"], "urban") + + +class CtaToneTests(SimpleTestCase): + """用户明确要求全系统去掉「小黄车」那套平台指令腔:CTA 要回到情境、点名谁适合。""" + + _VISUAL = ( + "0-3s:近景;平视;固定机位;女主在画面右侧对镜头抬眼\n" + "3-8s:中近景;侧后方过肩;手持跟拍;右手从画面右侧入画拧开瓶盖\n" + "8-12s:特写;俯拍杯口;缓慢推近;液体注入冰块,颜色渐染成琥珀\n" + "12-15s:中近景;平视;拉回;女主喝一口,眉头松开肩膀塌下来" + ) + + def _draft(self, narration): + return {"segments": [{"duration": 15, "role": "CTA", "narration": narration, "visual": self._VISUAL}]} + + def test_platform_shout_is_rejected(self): + for bad in ("现在点下方小黄车就能入手。", "别犹豫了,赶紧下单。", "想省事的直接买它。", "手慢无,冲。"): + with self.assertRaises(ValueError, msg=bad) as ctx: + assert_cta_is_not_shouty(self._draft(bad)) + self.assertIn("平台指令腔", str(ctx.exception)) + + def test_shout_in_hook_is_rejected_too(self): + with self.assertRaises(ValueError): + assert_cta_is_not_shouty({"hook": "闭眼入就完了", "segments": []}) + + def test_natural_contextual_cta_passes(self): + assert_cta_is_not_shouty(self._draft("你要是也天天下午三点犯困,可以试试这个,我一盒能喝小半个月。")) + assert_cta_is_not_shouty(self._draft("链接放这儿了,先看看合不合适再说,不着急。")) diff --git a/core/backend/apps/ai/test_video_caption_policy.py b/core/backend/apps/ai/test_video_caption_policy.py index 791363c..f6dacb7 100644 --- a/core/backend/apps/ai/test_video_caption_policy.py +++ b/core/backend/apps/ai/test_video_caption_policy.py @@ -1,9 +1,16 @@ from django.test import SimpleTestCase -from apps.ai.services import NO_EMBEDDED_CAPTIONS_REQUIREMENT, enforce_no_embedded_captions +from apps.ai.services import ( + NO_EMBEDDED_CAPTIONS_REQUIREMENT, + enforce_no_embedded_captions, + strip_caption_directives, +) class VideoCaptionPolicyTests(SimpleTestCase): + """全系统出的视频一律无字幕。所有入口(专业创作 / 一键成片 / 自由创作 / 视频复刻 / 提炼改写) + 都汇入 execute_routed_video_submit → enforce_no_embedded_captions,这里守住那一道。""" + def test_always_appends_no_embedded_captions_requirement(self): prompt = enforce_no_embedded_captions("一位用户在厨房展示商品") @@ -14,3 +21,38 @@ class VideoCaptionPolicyTests(SimpleTestCase): prompt = enforce_no_embedded_captions(NO_EMBEDDED_CAPTIONS_REQUIREMENT) self.assertEqual(prompt.count(NO_EMBEDDED_CAPTIONS_REQUIREMENT), 1) + + def test_requirement_covers_the_overlays_seedance_likes_to_invent(self): + """只写「不要字幕」时实测仍会冒出花字 / 价格贴片 / 购物浮层,禁令必须逐个点名。""" + for term in ("花字", "水印", "角标", "价格贴片", "弹幕", "购物车", "进度条", "片尾"): + self.assertIn(term, NO_EMBEDDED_CAPTIONS_REQUIREMENT, msg=f"禁令漏了「{term}」") + # 提示词别处若还残留「字幕」字样,以本条为准 —— 解决自相矛盾导致的偶发出字幕 + self.assertIn("以本条为准", NO_EMBEDDED_CAPTIONS_REQUIREMENT) + + def test_strips_caption_field_lines(self): + """整行的「字幕:xxx」直接丢掉 —— 模型看到具体字幕内容就会把它画进画面。""" + cleaned = strip_caption_directives("字幕:限时8折\n0-3s:近景;平视;女主抬眼") + + self.assertNotIn("限时8折", cleaned) + self.assertIn("0-3s:近景;平视;女主抬眼", cleaned) + + def test_strips_inline_caption_field_inside_sound_row(self): + """脚本【声音】栏里的「字幕:…」片段要摘掉,同一行其它内容留着。""" + cleaned = strip_caption_directives("【声音】口播;音效:倒水声;字幕:熬夜救星;背景音乐:无") + + self.assertNotIn("熬夜救星", cleaned) + self.assertIn("音效:倒水声", cleaned) + self.assertIn("背景音乐:无", cleaned) + + def test_does_not_touch_real_packaging_text(self): + """包装上的真实文字是允许出现的,别被误伤。""" + line = "0-3s:近景,包装上的品牌字样清晰" + + self.assertEqual(strip_caption_directives(line), line) + + def test_scrubbing_runs_before_the_requirement_is_appended(self): + prompt = enforce_no_embedded_captions("花字:手慢无\n0-3s:特写;推近;倒入") + + self.assertNotIn("手慢无", prompt) + self.assertIn("0-3s:特写;推近;倒入", prompt) + self.assertIn(NO_EMBEDDED_CAPTIONS_REQUIREMENT, prompt) diff --git a/core/backend/apps/ai/test_video_replace.py b/core/backend/apps/ai/test_video_replace.py index d39d72a..28dfef8 100644 --- a/core/backend/apps/ai/test_video_replace.py +++ b/core/backend/apps/ai/test_video_replace.py @@ -585,10 +585,9 @@ class SubmitVideoReplaceTests(TestCase): def test_character_prompt_swaps_person_not_product(self): prompt = build_character_replace_prompt(DIGEST_SAMPLE) self.assertIn("@目标角色", prompt) - self.assertIn("【镜头 01】", prompt) + self.assertIn("@参考视频", prompt) self.assertIn("商品", prompt) self.assertNotIn("@目标商品", prompt) - self.assertNotIn("@参考视频", prompt) def test_digest_generates_once_without_source_video(self): """拆完只提交一次 Seedance,不把用户原片传进去。""" @@ -628,9 +627,9 @@ class SubmitVideoReplaceTests(TestCase): task = self._submit(replace_mode="character", product_id="", model_id=str(model.id)) self.assertEqual(task.request_payload["replace_mode"], "character") self.assertEqual(task.request_payload["subject_name"], "薇薇") - self.assertTrue(task.request_payload["digest_pending"]) + self.assertFalse(task.request_payload["digest_pending"]) labels = [item["label"] for item in task.request_payload["references"]] - self.assertNotIn("参考视频", labels) + self.assertIn("参考视频", labels) self.assertIn("目标角色", labels) def test_both_modes_use_seedance_25_and_cap_at_30s(self): @@ -877,13 +876,13 @@ class VideoReplaceReviewGateTests(TestCase): grp.return_value = MagicMock(remote_group_id="Group-1") task = self._submit() self.assertEqual(task.status, AITask.Status.CREATED) - self.assertTrue((task.request_payload or {}).get("digest_pending")) + self.assertFalse((task.request_payload or {}).get("digest_pending")) self.assertEqual(CreditReservation.objects.filter(task=task).count(), 0) self.assertFalse(self.provider.create_video_task.called) passed_types = [call.kwargs.get("asset_type") for call in create.call_args_list] - self.assertEqual(passed_types, ["Image"]) + self.assertEqual(passed_types, ["Video", "Image"]) labels = [item["label"] for item in (task.request_payload or {}).get("references") or []] - self.assertNotIn("参考视频", labels) + self.assertIn("参考视频", labels) def test_advance_starts_generation_after_review_passes(self): with patch("apps.assets.assets_client.is_enabled", return_value=True), patch( @@ -899,18 +898,20 @@ class VideoReplaceReviewGateTests(TestCase): self.image.save(update_fields=["review_status", "review_remote_id"]) with patch("apps.assets.assets_client.is_enabled", return_value=True), patch( "apps.assets.assets_client.get_asset", return_value={"Status": "Active"} - ), patch( - "apps.ai.video_digest.digest_asset_video", - return_value=(DIGEST_SAMPLE, {"digest_shots": 2}), ): - run_replace_digest(task) + task = advance_video_replace(task) task.refresh_from_db() self.assertEqual(task.status, AITask.Status.SUBMITTED) self.assertTrue(CreditReservation.objects.filter(task=task).exists()) self.assertTrue(self.provider.create_video_task.called) content = self.provider.create_video_task.call_args.kwargs.get("content_items") or [] types = [item.get("type") for item in content] - self.assertNotIn("video_url", types) + self.assertIn("video_url", types) + video_urls = [ + (item.get("video_url") or {}).get("url") + for item in content if item.get("type") == "video_url" + ] + self.assertTrue(all(str(url).startswith("asset://") for url in video_urls if url)) image_urls = [ (item.get("image_url") or {}).get("url") for item in content if item.get("type") == "image_url" diff --git a/core/backend/apps/ai/video_replace.py b/core/backend/apps/ai/video_replace.py index dba943c..f09ef51 100644 --- a/core/backend/apps/ai/video_replace.py +++ b/core/backend/apps/ai/video_replace.py @@ -86,9 +86,9 @@ PRODUCT_PROMPT = ( "严格遵守每镜的新商品动作、场景与口播;不要恢复参考片原商品、原动作、原卖点或原台词。" ) CHARACTER_PROMPT = ( - "按下面这份分镜稿生成视频。镜头数量、每镜时长、景别、机位、运镜、商品、场景、口播全部按稿执行," - "不要增删镜头、不要改顺序。" - "把稿里的原人物换成@目标角色,五官、发型、体态以参考图为准。" + "以@参考视频的镜头、人物动作、场景、节奏、口播和剪辑为唯一基准生成视频。" + "将画面中所有可见的原人物替换为@目标角色;五官、发型、体态和服装以角色参考图为准。" + "保留原商品、场景、镜头运动、动作顺序和声音节奏,不添加字幕、花字或额外人物。" ) # 拆解期间的占位提示词。真正的提示词在 worker 拆完后回写。 @@ -625,11 +625,18 @@ def submit_video_replace(*, team, user, params: dict): "feature": FEATURE, } - # 分镜用「提炼提示词」同一套拆。原片只当拆解源,不进审核列表、也不传火山。 - from .video_digest import resolve_digest_model_config + # 商品复刻需要 Gemini 拆镜后重构商品语义;角色复刻直接交给 Seedance 2.5, + # 原视频与角色图都先进入同一火山素材库审核,再以 asset:// 作为参考。 + if replace_mode == "product": + from .video_digest import resolve_digest_model_config - if resolve_digest_model_config() is None: - raise ValueError("视频提炼模型未配置,请联系管理员") + if resolve_digest_model_config() is None: + raise ValueError("视频提炼模型未配置,请联系管理员") + else: + image_refs = [ + _owned_ref(video, kind="video", role="reference_video", label="参考视频"), + *image_refs, + ] # 商品图/角色图仍要过审才能当生成参考。提前送审 → 审核和提炼并行跑,明确不过审的直接 400。 review_state = _ensure_replace_refs_reviewed(team, image_refs) if review_state == "failed": @@ -648,11 +655,11 @@ def submit_video_replace(*, team, user, params: dict): user=user, params={ **base_params, - "prompt": DIGEST_PENDING_PROMPT, + "prompt": DIGEST_PENDING_PROMPT if replace_mode == "product" else CHARACTER_PROMPT, "references": image_refs, "extra_payload": extra, }, - digest_pending=True, + digest_pending=replace_mode == "product", ) @@ -670,7 +677,11 @@ def advance_video_replace(task): # worker 还在拆参考视频,提示词都没生成,别当成「审核中」反复送审。 # worker 挂了也不会永远卡着:CREATED 超 16 分钟由 _reap_stale_free_video_tasks 回收退费。 return task - references = _image_refs(payload.get("references") or []) + references = ( + _image_refs(payload.get("references") or []) + if payload.get("replace_mode") == "product" + else list(payload.get("references") or []) + ) try: state = _ensure_replace_refs_reviewed(task.team, references) except ValueError as exc: @@ -701,9 +712,10 @@ def advance_video_replace(task): def start_replace_generation(task): """审核过了:整份分镜稿一次交给 Seedance 2.5。只带商品图/角色图,不传用户原片。""" payload = dict(task.request_payload or {}) - payload["references"] = _image_refs(payload.get("references") or []) - payload.pop("shot_plan", None) - payload.pop("shot_total", None) + if payload.get("replace_mode") == "product": + payload["references"] = _image_refs(payload.get("references") or []) + payload.pop("shot_plan", None) + payload.pop("shot_total", None) task.request_payload = payload task.save(update_fields=["request_payload", "updated_at"]) return start_pending_free_video(task) @@ -719,6 +731,9 @@ def run_replace_digest(task) -> None: if not is_video_replace_task(task) or task.status != AITask.Status.CREATED: return payload = task.request_payload or {} + # 角色复刻没有 Gemini 拆镜环节;审核通过后由 advance_video_replace 直接提交 Seedance。 + if payload.get("replace_mode") == "character": + return if not payload.get("digest_pending"): return diff --git a/core/backend/skills/ecommerce-video-script/SKILL.md b/core/backend/skills/ecommerce-video-script/SKILL.md index 14f2410..010b74e 100644 --- a/core/backend/skills/ecommerce-video-script/SKILL.md +++ b/core/backend/skills/ecommerce-video-script/SKILL.md @@ -92,7 +92,7 @@ description: > - `entities[].id` 全局唯一,`segments[].entity_refs` 与 `speaker` 只能引用已声明的 id。 - **发声方式每镜自己判断**(不强求统一,看这一镜的内容和场景): - **旁白/口播** → 填 `narration`,`dialogue` 留空 `[]`(大多数电商镜是这种); - - **角色对话** → 填 `dialogue`:元素为 `{"speaker":"角色 entity 的 id,或 null=旁白","line":"台词"}`,每条 `line` ≤55 字;并把各 `line` 拼进 `narration` 兜底下游字幕/配音。剧情向、多角色互动、或一句自然的吐槽/接话更带感时都可以用,**不必非到「剧情」档**; + - **角色对话** → 填 `dialogue`:元素为 `{"speaker":"角色 entity 的 id,或 null=旁白","line":"台词"}`,每条 `line` ≤55 字;并把各 `line` 拼进 `narration` 兜底下游配音。剧情向、多角色互动、或一句自然的吐槽/接话更带感时都可以用,**不必非到「剧情」档**; - **纯画面展示** → `narration` 与 `dialogue` 都留空(没人说话,只有画面)。 - **判断权交给你**:依据用户输入与这一镜的功能/场景决定。**不是每一镜都得有对白,也不必死守旁白**;但**别无故给每镜都塞对白**(那样很假),自然才好。 - 发声方式还受**表现形式**约束:口播只用 `narration`、短剧必须用 `dialogue`、Vlog 以画外旁白为主。 @@ -113,7 +113,7 @@ description: > - **`visual` 固定使用导演层级**(四栏,逐栏写满,缺栏视为不合格): `【本镜任务】` 这一段要完成的情绪 / 信息变化 → `【光线氛围】` 光源方向与性质(窗光 / 顶光 / 台灯 / 逆光)、色温冷暖、明暗对比、整体色调 → - `【声音】` 台词 / 旁白状态、音效、背景音乐、字幕(没有写「无」)→ + `【声音】` 台词 / 旁白状态、音效、背景音乐(没有写「无」)→ **不写字幕栏**,本系统成片一律无字幕 → `【画面内容】` 下列 3–5 条秒级分镜。 不要把基础设定、画面风格、镜头动作、声音、旁白混成一段散文。 - **每镜 `visual` 必须按秒拆分镜**:15 秒一场里至少 3 刀、目标 4 刀,写成多行, @@ -130,8 +130,9 @@ description: > - **禁止评价词** —— 「展示商品」「呈现质感」「体现高级感」「氛围到位」「传递温暖」不是画面, 要写成「杯壁的水珠顺着往下滑」「她眉头松开,肩膀塌下来」。 - **禁止心理描写** —— 模型拍不出「她内心纠结」,要写「她拿起又放下,手指在包装边缘停了两秒」。 - - **禁止画面里出现文字** —— 不写字幕、花字、标题、价格贴片、UI、弹窗、对比图表; - 商品包装上原有的真实文字除外。 + - **禁止画面里出现文字** —— 本系统出的**任何视频都不带字幕**。不写字幕、花字、标题、片头片尾、 + 水印、角标、价格贴片、优惠标签、弹幕、对话气泡、贴纸、参数表、对比图表、UI、按钮、购物浮层; + 口播和台词只以声音存在。商品包装上原本就有的真实文字除外。 - **禁止一镜内跨场景 / 跨时间蒙太奇** —— 15 秒是**一个连续动作链**,同一地点、同一光线、同一套衣服。 要换环境就换到下一镜,并在 `entity_refs` 里换成另一个 scene。 - **一致性靠文字锁死**:同一角色 / 商品 / 场景的外观一律引用 entities 里的既定设定, @@ -140,7 +141,7 @@ description: > - **画面物理常识**:商品用法必须是真人会做的。茶 / 咖啡 = 热水、蒸汽、茶汤渐染,禁止茶包丢进看起来像冷白开的杯子; 护肤品 = 挤出 / 涂抹;食品 = 打开 / 入口。手从真实方向入画,禁止悬浮肢体、反关节、商品凭空出现。 每条分镜写清容器 / 包装长什么样、液体或材质当前状态。 - 输入是 `【镜头 01】` 分镜稿时,把原稿的景别、机位、运镜、动作、表情、音效、背景音乐、字幕、备注折进导演说明书,不要压成一句画面摘要。 + 输入是 `【镜头 01】` 分镜稿时,把原稿的景别、机位、运镜、动作、表情、音效、背景音乐、备注折进导演说明书,不要压成一句画面摘要;**原稿的「字幕 / 花字」一栏一律丢弃**,参考视频有字幕不代表我们的成片要有。 - 不要输出 schema 之外的字段,也不要省略必填字段。 ### 铁律 2 · 输出前自检 @@ -189,7 +190,7 @@ description: > | ---- | ---- | ---- | ---- | | **① 全自动** | 用户只给【商品信息 + 前置条件(表现形式/视频结构/时长/人群/卖点勾选)】,无主题无原稿 | 凭商品与前置条件,自动定镜数、选 tone、造 entity、按套路填镜 | `methodology.md` + `hook-library.md` + `category-playbook.md` + `platform-tone.md` | | **② 一句话** | 用户额外给了一句主题(如「主打熬夜党救星」) | 以该主题为脚本主轴扩写,其余同全自动 | 同上(主题优先于自动选题) | -| **③ 改稿 / 上传脚本 / 上传视频提炼** | 用户给了已有脚本,或一份 `【镜头 01】` 导演分镜稿 | **脚本**:保留用户原意,增强钩子/节奏/卖点证明/CTA,并归一化到 JSON。**视频分镜稿**:照搬镜头顺序、每镜时长比例、景别、机位、运镜、人物动作和声音层次,把人物、商品、品牌、台词换成当前商品;`visual` 必须把景别/机位/运镜/动作/表情/音效/背景音乐/字幕/备注折进导演说明书,不要压成一句画面摘要 | `methodology.md` + `hook-library.md` + `checklist.md` | +| **③ 改稿 / 上传脚本 / 上传视频提炼** | 用户给了已有脚本,或一份 `【镜头 01】` 导演分镜稿 | **脚本**:保留用户原意,增强钩子/节奏/卖点证明/CTA,并归一化到 JSON。**视频分镜稿**:照搬镜头顺序、每镜时长比例、景别、机位、运镜、人物动作和声音层次,把人物、商品、品牌、台词换成当前商品;`visual` 必须把景别/机位/运镜/动作/表情/音效/背景音乐/备注折进导演说明书(**丢弃原稿的字幕栏**),不要压成一句画面摘要 | `methodology.md` + `hook-library.md` + `checklist.md` | | 信息不足 | 连商品信息都缺,无法生成 | 唯一允许的回退:用一句话问清最少必要信息 | — | **进入任何模式前,必须先读取该行列出的参考资料。** diff --git a/core/backend/skills/ecommerce-video-script/references/checklist.md b/core/backend/skills/ecommerce-video-script/references/checklist.md index ed4c046..546b594 100644 --- a/core/backend/skills/ecommerce-video-script/references/checklist.md +++ b/core/backend/skills/ecommerce-video-script/references/checklist.md @@ -34,7 +34,8 @@ - [ ] 钩子镜第一句在前 3 秒抛出钩子,套用了 `hook-library.md` 的某个公式。 - [ ] 顶层 `hook` 与钩子镜口径一致。 - [ ] 卖点镜的卖点**挂在前面铺的痛点上**,不是干罗列参数。 -- [ ] CTA 给了明确动作(点小黄车/领券/主页链接)。**场景种草结构的 CTA 要软**,别硬喊。 +- [ ] CTA 回到了钩子那个情境,点名了谁适合,用一句自然口语收尾。**场景种草结构的 CTA 要更软**。 +- [ ] **CTA 禁用词扫描**:没有 小黄车 / 购物车 / 点下方 / 点击购买 / 立即购买 / 马上下单 / 赶紧下单 / 抢购 / 秒杀 / 手慢无 / 别犹豫 / 买它 / 闭眼入 / 上车(见 `methodology.md` 第七节)。 - [ ] **促销抢购**:所有价格、优惠、库存、赠品、开售时间和适用条件均来自商品资料;缺失就不写,绝不虚构限时限量。 - [ ] **知识分享**:知识点可由商品资料支持,商品是在判断方法建立后自然出现;没有虚构专业身份、数据、标准或功效。 - [ ] **结构骨架走完了**:段的顺序与功能符合 `playbooks/structure-*.md` 里那份骨架表。 @@ -50,10 +51,11 @@ - [ ] **每镜 `narration` ≤ `duration × 5.2` 字,且绝不超过 78 字**(15 秒镜 ≤78 字)。 - [ ] **口播 15 秒镜旁白 ≥ 64 字**(写作目标 66–74 字,4–5 句)。禁止一句 20 字收工;口播禁止整镜无声。 -- [ ] **秒级分镜**:每镜 `visual` 至少 3 条 `0-3s:景别;机位;运镜;动作;信息变化`,从 0 接到 15s;15 秒内至少切两次景别;**每镜至少出现一个运镜词**(手持跟拍/推近/拉远/横摇/环绕/固定机位)。上传视频提炼稿还要把音效/背景音乐/字幕折进 `【声音】`。 +- [ ] **秒级分镜**:每镜 `visual` 至少 3 条 `0-3s:景别;机位;运镜;动作;信息变化`,从 0 接到 15s;15 秒内至少切两次景别;**每镜至少出现一个运镜词**(手持跟拍/推近/拉远/横摇/环绕/固定机位)。上传视频提炼稿还要把音效/背景音乐折进 `【声音】`(**字幕栏丢弃**)。 - [ ] **四栏齐全**:每镜 `visual` 都有 `【本镜任务】【光线氛围】【声音】【画面内容】`,没有缺栏。 - [ ] **相邻镜光线衔接**:同一场戏各镜的光源方向、色温、色调一致,没有忽冷忽暖。 -- [ ] **画面可拍**:没有「展示商品/呈现质感/体现高级感」这类评价词,没有心理描写,没有画面内字幕花字价签,没有一镜内跨场景跨时间。 +- [ ] **画面可拍**:没有「展示商品/呈现质感/体现高级感」这类评价词,没有心理描写,没有一镜内跨场景跨时间。 +- [ ] **全片无字幕**:任何一镜的 `visual` 都没有要求字幕、花字、标题、水印、角标、价签、购物浮层或其它画面文字,`【声音】` 里也没有「字幕」这一栏。 - [ ] **商品原词**:旁白/对白出现了输入给出的至少一个勾选卖点原词;商品名全片点名 1–2 次即可,其余镜写描述/卖点细节,没有用空话替换。 - [ ] 每镜 `visual` 能撑满 15 秒(至少 110 字),写清哪只手、从哪个方向入画、握商品哪里,以及容器/材质的真实状态,不是一句静态动作。 - [ ] 口语化,无书面腔/AI 腔("综上""不仅…而且""值得一提"等已清除)。 diff --git a/core/backend/skills/ecommerce-video-script/references/methodology.md b/core/backend/skills/ecommerce-video-script/references/methodology.md index dae0642..9c5e061 100644 --- a/core/backend/skills/ecommerce-video-script/references/methodology.md +++ b/core/backend/skills/ecommerce-video-script/references/methodology.md @@ -13,11 +13,11 @@ | **钩子** | 3 秒留人 | 第一句话/第一帧就让人停下不划走 | 用 `hook-library.md` 的公式,制造痛点共鸣或反差冲击 | | **痛点** | 共鸣 | 把用户"对,我就这样"的处境讲出来 | 具体场景化,不抽象,让人对号入座 | | **卖点** | 商品解决 | 商品如何**恰好**解决上面的痛点 | 卖点落到痛点上,给"凭什么信"的理由(成分/效果/对比/口碑) | -| **CTA** | 转化行动 | 临门一脚,告诉用户现在做什么 | 明确动作 + 紧迫感(点下方小黄车 / 主页领券 / 限时) | +| **CTA** | 转化行动 | 回到开头那个具体情境,把商品交到「这样的人」手里 | 给谁 + 为什么值得试 + 一句自然的口语引导。**禁止喊平台指令**(小黄车 / 点下方 / 赶紧下单 / 抢购 / 手慢无) | **铁原则:** - **首镜永远是钩子**——3 秒内必须抛出钩子,否则用户划走。 -- **末镜永远收 CTA**——可以是独立 CTA 镜,也可以是末镜旁白末尾收一句行动指令。 +- **末镜永远收 CTA**——可以是独立 CTA 镜,也可以是末镜旁白末尾自然收一句。收的是「谁适合、为什么值得试」,不是喊口号。 - **卖点必须挂在痛点上**——不是罗列参数,是"这个痛点 → 商品这样解决"。 - **一条只讲一个核心情境**——先选一个「时间 + 地点 + 小麻烦/小欲望」,整条围绕它推进; 不要把商品详情页的所有卖点搬进来。 @@ -90,7 +90,7 @@ | 钩子 | 可弱露出或不露 | 钩子先抓人,商品可作悬念,第 1 镜末尾闪一下也行 | | 痛点 | 不露 / 反面对照 | 展示"没有它"的糟糕状态,商品先按住 | | 卖点 | **特写 / 使用中** | 商品成为主角:成分特写、质地特写、上脸/上手使用中 | -| CTA | 手持 / 包装正面 | 手持商品对镜头,包装正面清晰,配合行动指令 | +| CTA | 手持 / 包装正面 | 手持商品对镜头,包装正面清晰,人物状态已经变好(画面里不出现任何购物浮层或文字) | 露出方式词汇统一用:`手持` / `特写` / `使用中` / `包装正面` / `场景摆放` / `对比展示`(可组合,如"手持+特写")。 @@ -142,14 +142,14 @@ ``` 【本镜任务】本段要让观众看懂的变化或悬念 【光线氛围】光源方向与性质(窗光/顶光/台灯/逆光);色温冷暖;明暗对比;整体色调(各镜保持一致) -【声音】台词/旁白状态;音效;背景音乐;字幕(没有写无) +【声音】台词/旁白状态;音效;背景音乐(没有写无)—— 不写字幕栏,成片一律无字幕 【画面内容】 0-3s:景别;机位;运镜;主体在画面中的位置;具体动作;信息变化 3-8s:景别;机位;运镜;手与商品的空间关系;可见细节 ... ``` -输入如果是 `【镜头 01】` 导演分镜稿(上传视频提炼),把原稿每镜的景别、机位、运镜、人物动作、表情、音效、背景音乐、字幕、备注折进对应栏,不要压成一句画面摘要。原稿写「无 / 不可见 / 听不清」的不要编造。 +输入如果是 `【镜头 01】` 导演分镜稿(上传视频提炼),把原稿每镜的景别、机位、运镜、人物动作、表情、音效、背景音乐、备注折进对应栏,不要压成一句画面摘要。**原稿的「字幕 / 花字」一栏一律丢弃**,不搬进新脚本。原稿写「无 / 不可见 / 听不清」的不要编造。 这样「人物/商品/场景设定」留在 entities 中保持全片一致,「本镜任务」管情绪推进, 「光线氛围」锁住全片视觉调性(原来这件事由分镜图承担,现在只能靠文字), @@ -169,8 +169,41 @@ - **每镜至少出现一个运镜词**(手持跟拍 / 推近 / 拉远 / 横摇 / 环绕 / 固定机位); 没有运镜词,出片会拍成呆板的静止画面。 - **只写拍得出来的东西**:禁止「展示商品 / 呈现质感 / 体现高级感」这类评价词,禁止心理描写, - 禁止画面里出现字幕花字标题价签,禁止一镜内跨场景跨时间。 + 禁止画面里出现任何文字叠加(字幕 / 花字 / 标题 / 水印 / 角标 / 价签 / 购物浮层),口播只以声音存在;禁止一镜内跨场景跨时间。 - 商品用法必须真实:茶要热水和蒸汽,不要把茶包丢进冷白开;手不要悬浮。 - **一场一个动作链**:把 15 秒拆成同一件事的起因 → 操作 → 可见变化 → 反应,不能把无关的 剧情、多个卖点、跳场景硬塞进同一场。画面提供证据,台词提供判断或转折,不要互相复述。 - 允许另给 `beats:[{start,end,shot_size,action}]`,但 `visual` 仍要能折成上面这种多行文本。 + +--- + +## 七、CTA 怎么写(硬规则) + +**旧写法已废弃。** 「点下方小黄车」「赶紧下单」「手慢无」这类平台指令腔一律禁止 —— +它把一条好片子瞬间打回广告,观众听到就划走,而且换平台就穿帮。 + +### 禁用词(出现即不合格) + +小黄车、购物车、点下方、点击下方、点击购买、立即购买、马上下单、赶紧下单、抢购、秒杀、 +手慢无、别犹豫、冲、买它、闭眼入、直接拍、上车。 + +### 合格的 CTA = 三件事,一句话说完 + +1. **回到钩子那个情境** —— 不是泛泛的"推荐给大家",是"你要是也……" +2. **点名谁适合** —— 具体到人群或处境(久坐犯困的、洗头后半天吹不干的、租房没灶台的) +3. **一句自然的引导** —— 像跟朋友说话,给个理由,不下命令 + +### 参考写法(照着这个语气写,别照抄原句) + +- 「你要是也天天下午三点犯困,可以试试这个,我一盒能喝小半个月。」 +- 「链接放这儿了,先看看合不合适再说,不着急。」 +- 「租房没灶台的,这个是真省事,我用了俩月了。」 +- 「想少踩点坑,就照我说的这两个点挑,别的其实差不太多。」 + +### 还要注意 + +- **一个 CTA 就够**,不要既让人下单又让人关注。 +- **紧迫感只能来自真实事实**(商品资料里写明的活动 / 规格 / 库存);没有事实就不制造紧迫感, + 更不许编"最后一天""仅剩 3 件"。 +- **绝对化用语照旧禁止**(最 / 第一 / 唯一 / 100% / 全网最低)。 +- **画面里不出现任何购买入口、价格贴片或浮层** —— 成片无文字叠加,转化靠说得对,不靠贴图。 diff --git a/core/backend/skills/ecommerce-video-script/references/platform-tone.md b/core/backend/skills/ecommerce-video-script/references/platform-tone.md index adcb2e6..f6e616b 100644 --- a/core/backend/skills/ecommerce-video-script/references/platform-tone.md +++ b/core/backend/skills/ecommerce-video-script/references/platform-tone.md @@ -10,10 +10,10 @@ ## 抖音 - **节奏**:快、强钩子、信息密集,前 3 秒定生死,反转/爽点前置。 -- **风格**:高能、口播感强、配合小黄车,直给卖点。 +- **风格**:高能、口播感强,直给卖点。 - **钩子偏好**:痛点提问、数字冲击、反差。 - **tone 倾向**:`种草` / `痛点`。 -- **定调**:开门见山抓人,节奏别拖,CTA 干脆("点下方小黄车")。 +- **定调**:开门见山抓人,节奏别拖,CTA 干脆但不喊口号("你要是也这样,真该试试",不是"点下方小黄车")。 ## 快手 @@ -29,7 +29,7 @@ - **风格**:精致、审美在线、第一人称"我亲测",软种草不硬推。 - **钩子偏好**:悬念留白、反差、身份代入(细分人群)。 - **tone 倾向**:`种草` / `测评`。 -- **定调**:真诚分享感,画面要美,卖点融进使用体验,CTA 柔("主页有链接/评论区扣")。 +- **定调**:真诚分享感,画面要美,卖点融进使用体验,CTA 柔("合不合适你自己看,我就用着舒服")。 ## 视频号 diff --git a/core/backend/skills/ecommerce-video-script/references/playbooks/combo-matrix.md b/core/backend/skills/ecommerce-video-script/references/playbooks/combo-matrix.md index 0dd9042..7ed1ca0 100644 --- a/core/backend/skills/ecommerce-video-script/references/playbooks/combo-matrix.md +++ b/core/backend/skills/ecommerce-video-script/references/playbooks/combo-matrix.md @@ -80,7 +80,7 @@ ### Vlog × 前后对比 用**时间跨度**做对比,Vlog 天然适合记录「第 1 天 / 第 7 天」。 - 每个时间点用同机位拍一次,形成序列 -- 画面上标时间(Day 1 / Day 7),这是 Vlog 里唯一允许的字幕硬信息 +- 时间推进(Day 1 / Day 7)只能靠画面本身表达(光线、穿着、商品用量、日历道具),**不能打字幕** —— 成片一律无文字叠加 - 中间的日常镜头就是「过程可见」,可信度天然高 ### Vlog × 测评验证 diff --git a/core/backend/skills/ecommerce-video-script/references/playbooks/format-drama.md b/core/backend/skills/ecommerce-video-script/references/playbooks/format-drama.md index e691062..3d9dfc8 100644 --- a/core/backend/skills/ecommerce-video-script/references/playbooks/format-drama.md +++ b/core/backend/skills/ecommerce-video-script/references/playbooks/format-drama.md @@ -65,7 +65,7 @@ 短剧是**唯一必须用 `dialogue` 的形式**。 - `dialogue` 填角色对白:`[{"speaker":"c1","line":"..."},{"speaker":"c2","line":"..."}]` -- `narration` 把各句 `line` 拼起来兜底(下游字幕和配音要用) +- `narration` 把各句 `line` 拼起来兜底(下游配音要用;成片不打字幕) - 允许个别镜是纯画面(`narration` 和 `dialogue` 都空),用来给情绪留白 - **别每一镜都塞满对白** —— 有沉默的镜,情绪才有落点 diff --git a/core/backend/skills/ecommerce-video-script/references/playbooks/format-vlog.md b/core/backend/skills/ecommerce-video-script/references/playbooks/format-vlog.md index ce4aa41..7cf1f79 100644 --- a/core/backend/skills/ecommerce-video-script/references/playbooks/format-vlog.md +++ b/core/backend/skills/ecommerce-video-script/references/playbooks/format-vlog.md @@ -81,5 +81,5 @@ Vlog 的镜头必须有**手持感和生活质感**,这是它的可信度来 - ❌ **给商品硬广特写** —— 商品只在「被使用」时出现 - ❌ **旁白讲参数** —— 参数是口播和测评的活,Vlog 只讲感受 - ❌ **时间线跳跃混乱** —— 时间必须单向推进,不许倒叙 -- ❌ **强行喊 CTA** —— Vlog 的 CTA 要软(「链接我放下面了」而不是「快点小黄车」) +- ❌ **强行喊 CTA** —— Vlog 的 CTA 最软:给感受和适用人群就够(「我这俩月是真离不开了,你要是也……」),禁止任何平台指令腔 - ❌ **摆拍痕迹重** —— 完美构图反而失真,允许不完美 diff --git a/core/backend/skills/ecommerce-video-script/references/playbooks/structure-pain.md b/core/backend/skills/ecommerce-video-script/references/playbooks/structure-pain.md index a9ba147..5bcc789 100644 --- a/core/backend/skills/ecommerce-video-script/references/playbooks/structure-pain.md +++ b/core/backend/skills/ecommerce-video-script/references/playbooks/structure-pain.md @@ -70,7 +70,7 @@ ## 五、结果与 CTA - **结果必须可见**:同一个场景、同一个时间点,状态变了 -- CTA 给**一个**明确动作,不要给两个(「点小黄车」和「关注我」二选一) +- CTA 只收**一件事**,不要既让人买又让人关注;且必须是自然口语,禁止平台指令腔(见 `methodology.md` 第七节禁用词) - 紧迫感可以有,但不许用绝对化用语(「最后一天」可以,「全网最低」不行) --- diff --git a/core/backend/skills/ecommerce-video-script/references/playbooks/structure-promo.md b/core/backend/skills/ecommerce-video-script/references/playbooks/structure-promo.md index 298167e..fe46ce5 100644 --- a/core/backend/skills/ecommerce-video-script/references/playbooks/structure-promo.md +++ b/core/backend/skills/ecommerce-video-script/references/playbooks/structure-promo.md @@ -15,7 +15,7 @@ - 先说清适合谁、什么使用场景或组合需求,不能一开场只喊“抢”。 - 价格、优惠、赠品、库存、开售时间、适用人群和领取条件,只能使用输入商品资料里明确给出的事实。 - 商品露出优先包装正面、规格/组合细节和真实使用场景;画面不能用伪造倒计时、销量或库存数字。 -- CTA 说明一个购买入口和一个操作,例如“从商品卡选择这个规格”;没有活动信息时,只引导“查看商品详情”。 +- CTA 说清「这个规格适合谁、为什么现在值得」,用自然口语收;没有活动信息时只说值不值得看看。禁止喊平台控件名(小黄车 / 点下方 / 赶紧下单,见 `methodology.md` 第七节)。 ## 三、红线 diff --git a/core/frontend/src/routes/video-remix.tsx b/core/frontend/src/routes/video-remix.tsx index f65fb52..510aaf2 100644 --- a/core/frontend/src/routes/video-remix.tsx +++ b/core/frontend/src/routes/video-remix.tsx @@ -286,12 +286,6 @@ export function VideoRemixPage({ textModels = [], onNotify, onBack, navigate }: if (blobPreviewUrl) URL.revokeObjectURL(blobPreviewUrl); }, [blobPreviewUrl]); - useEffect(() => { - const el = promptRef.current; - if (!el || !hasResult) return; - el.style.height = "auto"; - el.style.height = `${Math.max(132, el.scrollHeight)}px`; - }, [prompt, hasResult]); useEffect(() => { if (!pollId) return; diff --git a/core/frontend/src/video-remix-page.css b/core/frontend/src/video-remix-page.css index 6ce3ef4..f0173b4 100644 --- a/core/frontend/src/video-remix-page.css +++ b/core/frontend/src/video-remix-page.css @@ -393,8 +393,10 @@ .vr-page .remix-prompt-panel .analysis-prompt { width: 100%; - min-height: 132px; - overflow-y: hidden; + height: 162px; + min-height: 162px; + max-height: 162px; + overflow-y: auto; resize: none; padding: 15px; border: 1px solid rgba(0, 47, 167, 0.14); @@ -1304,8 +1306,9 @@ .vr-page .remix-history-prompt textarea { width: 100%; - min-height: 360px; - max-height: 560px; + height: 162px; + min-height: 162px; + max-height: 162px; display: block; resize: none; overflow-y: auto;