优化脚本

This commit is contained in:
Azmat@qq.com
2026-08-31 11:57:34 +08:00
parent f59e9d0418
commit cbd4f362d9
20 changed files with 261 additions and 74 deletions
+1 -1
View File
@@ -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,
+3 -2
View File
@@ -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)
+44 -8
View File
@@ -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(
+32 -4
View File
@@ -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)
+13 -12
View File
@@ -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"
+28 -13
View File
@@ -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
@@ -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` 固定使用导演层级**(四栏,逐栏写满,缺栏视为不合格):
`【本镜任务】` 这一段要完成的情绪 / 信息变化 →
`【光线氛围】` 光源方向与性质(窗光 / 顶光 / 台灯 / 逆光)、色温冷暖、明暗对比、整体色调 →
`【声音】` 台词 / 旁白状态、音效、背景音乐、字幕(没有写「无」)→
`【声音】` 台词 / 旁白状态、音效、背景音乐(没有写「无」)→ **不写字幕栏**,本系统成片一律无字幕 →
`【画面内容】` 下列 35 条秒级分镜。
不要把基础设定、画面风格、镜头动作、声音、旁白混成一段散文。
- **每镜 `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` 第七节)
## 三、红线
-6
View File
@@ -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;
+7 -4
View File
@@ -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;