优化脚本
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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("链接放这儿了,先看看合不合适再说,不着急。"))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -86,9 +86,9 @@ PRODUCT_PROMPT = (
|
||||
"严格遵守每镜的新商品动作、场景与口播;不要恢复参考片原商品、原动作、原卖点或原台词。"
|
||||
)
|
||||
CHARACTER_PROMPT = (
|
||||
"按下面这份分镜稿生成视频。镜头数量、每镜时长、景别、机位、运镜、商品、场景、口播全部按稿执行,"
|
||||
"不要增删镜头、不要改顺序。"
|
||||
"把稿里的原人物换成@目标角色,五官、发型、体态以参考图为准。"
|
||||
"以@参考视频的镜头、人物动作、场景、节奏、口播和剪辑为唯一基准生成视频。"
|
||||
"将画面中所有可见的原人物替换为@目标角色;五官、发型、体态和服装以角色参考图为准。"
|
||||
"保留原商品、场景、镜头运动、动作顺序和声音节奏,不添加字幕、花字或额外人物。"
|
||||
)
|
||||
|
||||
# 拆解期间的占位提示词。真正的提示词在 worker 拆完后回写。
|
||||
@@ -625,11 +625,18 @@ def submit_video_replace(*, team, user, params: dict):
|
||||
"feature": FEATURE,
|
||||
}
|
||||
|
||||
# 分镜用「提炼提示词」同一套拆。原片只当拆解源,不进审核列表、也不传火山。
|
||||
# 商品复刻需要 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("视频提炼模型未配置,请联系管理员")
|
||||
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,6 +712,7 @@ def advance_video_replace(task):
|
||||
def start_replace_generation(task):
|
||||
"""审核过了:整份分镜稿一次交给 Seedance 2.5。只带商品图/角色图,不传用户原片。"""
|
||||
payload = dict(task.request_payload or {})
|
||||
if payload.get("replace_mode") == "product":
|
||||
payload["references"] = _image_refs(payload.get("references") or [])
|
||||
payload.pop("shot_plan", None)
|
||||
payload.pop("shot_total", None)
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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` |
|
||||
| 信息不足 | 连商品信息都缺,无法生成 | 唯一允许的回退:用一句话问清最少必要信息 | — |
|
||||
|
||||
**进入任何模式前,必须先读取该行列出的参考资料。**
|
||||
|
||||
@@ -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 腔("综上""不仅…而且""值得一提"等已清除)。
|
||||
|
||||
@@ -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% / 全网最低)。
|
||||
- **画面里不出现任何购买入口、价格贴片或浮层** —— 成片无文字叠加,转化靠说得对,不靠贴图。
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
## 抖音
|
||||
|
||||
- **节奏**:快、强钩子、信息密集,前 3 秒定生死,反转/爽点前置。
|
||||
- **风格**:高能、口播感强、配合小黄车,直给卖点。
|
||||
- **风格**:高能、口播感强,直给卖点。
|
||||
- **钩子偏好**:痛点提问、数字冲击、反差。
|
||||
- **tone 倾向**:`种草` / `痛点`。
|
||||
- **定调**:开门见山抓人,节奏别拖,CTA 干脆("点下方小黄车")。
|
||||
- **定调**:开门见山抓人,节奏别拖,CTA 干脆但不喊口号("你要是也这样,真该试试",不是"点下方小黄车")。
|
||||
|
||||
## 快手
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
- **风格**:精致、审美在线、第一人称"我亲测",软种草不硬推。
|
||||
- **钩子偏好**:悬念留白、反差、身份代入(细分人群)。
|
||||
- **tone 倾向**:`种草` / `测评`。
|
||||
- **定调**:真诚分享感,画面要美,卖点融进使用体验,CTA 柔("主页有链接/评论区扣")。
|
||||
- **定调**:真诚分享感,画面要美,卖点融进使用体验,CTA 柔("合不合适你自己看,我就用着舒服")。
|
||||
|
||||
## 视频号
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
### Vlog × 前后对比
|
||||
用**时间跨度**做对比,Vlog 天然适合记录「第 1 天 / 第 7 天」。
|
||||
- 每个时间点用同机位拍一次,形成序列
|
||||
- 画面上标时间(Day 1 / Day 7),这是 Vlog 里唯一允许的字幕硬信息
|
||||
- 时间推进(Day 1 / Day 7)只能靠画面本身表达(光线、穿着、商品用量、日历道具),**不能打字幕** —— 成片一律无文字叠加
|
||||
- 中间的日常镜头就是「过程可见」,可信度天然高
|
||||
|
||||
### Vlog × 测评验证
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
短剧是**唯一必须用 `dialogue` 的形式**。
|
||||
|
||||
- `dialogue` 填角色对白:`[{"speaker":"c1","line":"..."},{"speaker":"c2","line":"..."}]`
|
||||
- `narration` 把各句 `line` 拼起来兜底(下游字幕和配音要用)
|
||||
- `narration` 把各句 `line` 拼起来兜底(下游配音要用;成片不打字幕)
|
||||
- 允许个别镜是纯画面(`narration` 和 `dialogue` 都空),用来给情绪留白
|
||||
- **别每一镜都塞满对白** —— 有沉默的镜,情绪才有落点
|
||||
|
||||
|
||||
@@ -81,5 +81,5 @@ Vlog 的镜头必须有**手持感和生活质感**,这是它的可信度来
|
||||
- ❌ **给商品硬广特写** —— 商品只在「被使用」时出现
|
||||
- ❌ **旁白讲参数** —— 参数是口播和测评的活,Vlog 只讲感受
|
||||
- ❌ **时间线跳跃混乱** —— 时间必须单向推进,不许倒叙
|
||||
- ❌ **强行喊 CTA** —— Vlog 的 CTA 要软(「链接我放下面了」而不是「快点小黄车」)
|
||||
- ❌ **强行喊 CTA** —— Vlog 的 CTA 最软:给感受和适用人群就够(「我这俩月是真离不开了,你要是也……」),禁止任何平台指令腔
|
||||
- ❌ **摆拍痕迹重** —— 完美构图反而失真,允许不完美
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
## 五、结果与 CTA
|
||||
|
||||
- **结果必须可见**:同一个场景、同一个时间点,状态变了
|
||||
- CTA 给**一个**明确动作,不要给两个(「点小黄车」和「关注我」二选一)
|
||||
- CTA 只收**一件事**,不要既让人买又让人关注;且必须是自然口语,禁止平台指令腔(见 `methodology.md` 第七节禁用词)
|
||||
- 紧迫感可以有,但不许用绝对化用语(「最后一天」可以,「全网最低」不行)
|
||||
|
||||
---
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
- 先说清适合谁、什么使用场景或组合需求,不能一开场只喊“抢”。
|
||||
- 价格、优惠、赠品、库存、开售时间、适用人群和领取条件,只能使用输入商品资料里明确给出的事实。
|
||||
- 商品露出优先包装正面、规格/组合细节和真实使用场景;画面不能用伪造倒计时、销量或库存数字。
|
||||
- CTA 说明一个购买入口和一个操作,例如“从商品卡选择这个规格”;没有活动信息时,只引导“查看商品详情”。
|
||||
- CTA 说清「这个规格适合谁、为什么现在值得」,用自然口语收;没有活动信息时只说值不值得看看。禁止喊平台控件名(小黄车 / 点下方 / 赶紧下单,见 `methodology.md` 第七节)。
|
||||
|
||||
## 三、红线
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user