视频复刻添加描述框
This commit is contained in:
@@ -700,6 +700,48 @@ class SubmitVideoReplaceTests(TestCase):
|
||||
self.assertFalse(task.request_payload.get("shot_plan"))
|
||||
self.assertNotIn("@参考视频", task.request_payload["prompt"])
|
||||
|
||||
def test_subject_brief_reaches_product_facts(self):
|
||||
"""第 3 步填的商品信息要进 facts,并把「仅外观」升级成「部分」。"""
|
||||
task = self._submit(subject_brief=" 素颜霜,膏体按压泵,主打提亮 ")
|
||||
payload = task.request_payload
|
||||
self.assertEqual(payload["subject_brief"], "素颜霜,膏体按压泵,主打提亮")
|
||||
facts = payload["product_facts"]
|
||||
self.assertEqual(facts["用户补充信息"], "素颜霜,膏体按压泵,主打提亮")
|
||||
self.assertEqual(facts["资料状态"], "部分")
|
||||
|
||||
def test_character_brief_drives_voice_gender(self):
|
||||
"""角色信息里的性别要变成写死的声线要求。
|
||||
|
||||
这条守的是「女换男之后画面是男的、声音还是女的」:提示词以前引用了
|
||||
一个根本不存在的「系统提供的目标声音设定」,模型只能猜,于是沿用原音轨。
|
||||
"""
|
||||
from apps.ai.video_replace import build_character_submit_prompt
|
||||
|
||||
male = build_character_submit_prompt("男性,30 岁左右,中低音域")
|
||||
self.assertIn("成年男性声线", male)
|
||||
self.assertNotIn("成年女性声线", male)
|
||||
self.assertIn("男性,30 岁左右", male)
|
||||
# 前缀必须原样保留:老任务靠 prompt 前缀认 replace_mode
|
||||
self.assertTrue(male.startswith(CHARACTER_PROMPT))
|
||||
|
||||
self.assertIn("成年女性声线", build_character_submit_prompt("女性,25 岁,声音甜美"))
|
||||
# 男女都提到(「把女的换成男的」)时不下断言,交给通用规则
|
||||
self.assertNotIn("【目标声音设定】", build_character_submit_prompt("把女的换成男的"))
|
||||
# 没填也要有性别跟随规则,不能再引用不存在的设定
|
||||
bare = build_character_submit_prompt("")
|
||||
self.assertIn("必须随目标角色改变", bare)
|
||||
self.assertNotIn("系统提供的目标声音设定", bare)
|
||||
|
||||
def test_character_brief_lands_in_submitted_prompt(self):
|
||||
portrait = _asset(self.team, self.user, name="模特.png", preview="http://tos/model.png")
|
||||
model = Model.objects.create(team=self.team, name="阿哲", portrait_asset=portrait)
|
||||
task = self._submit(
|
||||
replace_mode="character", product_id="", model_id=str(model.id),
|
||||
subject_brief="男性,稳重,中低音域",
|
||||
)
|
||||
self.assertEqual(task.request_payload["subject_brief"], "男性,稳重,中低音域")
|
||||
self.assertIn("成年男性声线", task.request_payload["prompt"])
|
||||
|
||||
def test_character_library_and_temp_are_exclusive(self):
|
||||
portrait = _asset(self.team, self.user, name="模特.png", preview="http://tos/model.png")
|
||||
model = Model.objects.create(team=self.team, name="薇薇", portrait_asset=portrait)
|
||||
|
||||
@@ -95,7 +95,9 @@ CHARACTER_PROMPT = (
|
||||
"【声音重建规则——最高优先级】被替换人物对应的所有声音必须同步替换,包括画面内对白、口播、旁白、画外音、"
|
||||
"语气词和呼吸声。只保留原口播的文字内容、信息含义、句子顺序、出现时间、停顿位置、情绪类型和表达节奏;"
|
||||
"不得保留原人物的声线、音色、音高、年龄感、性别特征、口音、声纹或其他说话人身份特征。"
|
||||
"根据@目标角色的身份及系统提供的目标声音设定,重新生成全部相关人声。"
|
||||
"根据@目标角色的形象、下方【目标角色设定】,以及参考图中可见的性别与年龄特征,重新生成全部相关人声。"
|
||||
"当@目标角色与参考视频中原人物的性别或年龄段不同时,重建人声的性别音色必须随目标角色改变:"
|
||||
"男性角色必须用男声,女性角色必须用女声,绝不允许沿用原人物的性别音色。"
|
||||
"从视频开始到结束,被替换人物的声音必须始终使用同一个目标声音;不得出现原人物声线残留、声音身份中途变化、"
|
||||
"新旧声音混用,或画面角色已经替换但声音仍属于原人物的情况。"
|
||||
"画面中目标角色的嘴部动作必须与重新生成的人声一致,口型、台词开始时间、停顿、语速和情绪表达保持同步。"
|
||||
@@ -103,6 +105,51 @@ CHARACTER_PROMPT = (
|
||||
"如保留原视频声音与目标角色声音冲突,必须舍弃原人物声音,以重新生成的目标声音为最高优先级。"
|
||||
)
|
||||
|
||||
# 用户在第 3 步填的补充信息(商品名/品类,或角色性别年龄音色)。选填,不填维持原行为。
|
||||
MAX_SUBJECT_BRIEF = 500
|
||||
|
||||
|
||||
def _clean_subject_brief(value) -> str:
|
||||
return str(value or "").strip()[:MAX_SUBJECT_BRIEF]
|
||||
|
||||
|
||||
def _voice_gender_directive(brief: str) -> str:
|
||||
"""从用户填的角色信息里读出性别 → 写死声线要求。
|
||||
|
||||
Seedance 拿不到明确性别时倾向于沿用原音轨声线,女换男会出现
|
||||
「画面换成男的、声音还是女的」。用户明确写了性别就不再让模型猜。
|
||||
男女都提到(例如「把女的换成男的」)时不下断言,交给上面的通用规则。
|
||||
"""
|
||||
has_male = "男" in brief
|
||||
has_female = "女" in brief
|
||||
if has_male and not has_female:
|
||||
return (
|
||||
"【目标声音设定】目标角色为男性:重建的全部人声必须是成年男性声线,中低音域,胸腔共鸣,"
|
||||
"绝不允许出现女性音色、女性音高或偏女的中性声线。"
|
||||
)
|
||||
if has_female and not has_male:
|
||||
return (
|
||||
"【目标声音设定】目标角色为女性:重建的全部人声必须是成年女性声线,中高音域,"
|
||||
"绝不允许出现男性音色或男性音高。"
|
||||
)
|
||||
return ""
|
||||
|
||||
|
||||
def build_character_submit_prompt(subject_brief: str = "") -> str:
|
||||
"""角色复刻的提交提示词 = 固定规则 + 用户填的角色设定 + 推导出的声线要求。
|
||||
|
||||
⚠️ CHARACTER_PROMPT 必须留在开头:老任务靠 prompt 前缀认 replace_mode。
|
||||
"""
|
||||
brief = _clean_subject_brief(subject_brief)
|
||||
parts = [CHARACTER_PROMPT]
|
||||
if brief:
|
||||
parts.append(f"【目标角色设定】(用户填写,形象与声音的依据){brief}")
|
||||
voice = _voice_gender_directive(brief)
|
||||
if voice:
|
||||
parts.append(voice)
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
# 拆解期间的占位提示词。真正的提示词在 worker 拆完后回写。
|
||||
DIGEST_PENDING_PROMPT = "正在提炼参考视频的分镜稿…"
|
||||
|
||||
@@ -668,6 +715,7 @@ def serialize_video_replace_task(task, *, include_deleted_assets: bool = False)
|
||||
"replace_mode": replace_mode,
|
||||
"subject_name": payload.get("subject_name") or "",
|
||||
"subject_source": payload.get("subject_source") or "",
|
||||
"subject_brief": payload.get("subject_brief") or "",
|
||||
"product_id": payload.get("product_id") or "",
|
||||
"model_id": payload.get("model_id") or "",
|
||||
"review_stage": "reviewing" if reviewing else "",
|
||||
@@ -726,6 +774,9 @@ def submit_video_replace(*, team, user, params: dict):
|
||||
if video_seconds > REPLACE_REF_DURATION_MAX:
|
||||
raise ValueError("参考视频不能超过 30 秒,请剪短后重试")
|
||||
|
||||
# 第 3 步用户填的补充信息。商品:名称/品类/卖点;角色:性别/年龄/音色。选填。
|
||||
subject_brief = _clean_subject_brief(params.get("subject_brief"))
|
||||
|
||||
if has_product:
|
||||
subject_name, image_refs, subject_source = _product_library_refs(team, product_id)
|
||||
product_facts = _product_semantic_facts_for_id(team, product_id)
|
||||
@@ -744,6 +795,17 @@ def submit_video_replace(*, team, user, params: dict):
|
||||
"禁止推断": "不得编造功效、成分、认证和任何具体卖点;口播只描述看得见的外观、质地和使用感受。",
|
||||
}
|
||||
|
||||
# 用户手填的商品信息优先级最高:临时上传时这往往是唯一的品类来源,
|
||||
# 商品库选品时它补充库里没填的字段。填了就不再是「仅外观」。
|
||||
if replace_mode == "product" and subject_brief:
|
||||
product_facts = {
|
||||
**product_facts,
|
||||
"用户补充信息": subject_brief,
|
||||
"用户补充信息的权威性": "这是用户手动填写的商品事实,可直接作为品类、用途和卖点的依据,优先级高于据图推断。",
|
||||
}
|
||||
if product_facts.get("资料状态") == "仅外观":
|
||||
product_facts["资料状态"] = "部分"
|
||||
|
||||
# 角色替换是 Seedance 视频编辑:火山要求比例/时长跟随输入视频,不能由前端指定。
|
||||
duration = -1 if replace_mode == "character" else _output_duration(params.get("duration"), video_seconds)
|
||||
billing_ratio = str(params.get("aspect_ratio") or "9:16")
|
||||
@@ -756,6 +818,7 @@ def submit_video_replace(*, team, user, params: dict):
|
||||
"product_id": str(product_id) if product_id else "",
|
||||
"model_id": str(model_id) if model_id else "",
|
||||
"product_facts": product_facts,
|
||||
"subject_brief": subject_brief,
|
||||
# adaptive / -1 只用于火山请求;预估积分仍按参考视频实际时长和上传时检测出的比例计算。
|
||||
"billing_duration": max(4, int(round(video_seconds or 4))),
|
||||
"billing_aspect_ratio": billing_ratio,
|
||||
@@ -802,7 +865,10 @@ def submit_video_replace(*, team, user, params: dict):
|
||||
user=user,
|
||||
params={
|
||||
**base_params,
|
||||
"prompt": DIGEST_PENDING_PROMPT if replace_mode == "product" else CHARACTER_PROMPT,
|
||||
"prompt": (
|
||||
DIGEST_PENDING_PROMPT if replace_mode == "product"
|
||||
else build_character_submit_prompt(subject_brief)
|
||||
),
|
||||
"references": image_refs,
|
||||
"extra_payload": extra,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user