视频复刻优化替换商品和替换角色
This commit is contained in:
@@ -86,11 +86,21 @@ PRODUCT_PROMPT = (
|
||||
"严格遵守每镜的新商品动作、场景与口播;不要恢复参考片原商品、原动作、原卖点或原台词。"
|
||||
)
|
||||
CHARACTER_PROMPT = (
|
||||
"以@参考视频的镜头、人物动作、场景、节奏、口播和剪辑为唯一基准生成视频。"
|
||||
"将画面中所有可见的原人物替换为@目标角色;五官、发型、体态和服装以角色参考图为准。"
|
||||
"保留原商品、场景、镜头运动、动作顺序、语速、停顿和情绪节奏,不添加字幕、花字或额外人物。"
|
||||
"绝不继承参考视频中任何人的原始声线或人声:所有对白、旁白、口型和声音都必须与@目标角色的性别与身份一致,"
|
||||
"目标角色为男性时使用自然成年男声,目标角色为女性时使用自然成年女声。"
|
||||
"@参考视频仅用于参考镜头构图、镜头运动、人物动作、动作顺序、场景、商品、剪辑结构、画面节奏,"
|
||||
"以及原台词表达的文字语义和时间结构。"
|
||||
"将@参考视频中的原人物替换为@目标角色。目标人物的五官、发型、体态、年龄感、服装和整体形象,"
|
||||
"以@目标角色为唯一基准。"
|
||||
"保留原视频中的商品、场景、人物动作、镜头运动、动作顺序、剪辑点、画面节奏和故事表达,"
|
||||
"不添加字幕、花字、额外人物或无关元素。"
|
||||
"【声音重建规则——最高优先级】被替换人物对应的所有声音必须同步替换,包括画面内对白、口播、旁白、画外音、"
|
||||
"语气词和呼吸声。只保留原口播的文字内容、信息含义、句子顺序、出现时间、停顿位置、情绪类型和表达节奏;"
|
||||
"不得保留原人物的声线、音色、音高、年龄感、性别特征、口音、声纹或其他说话人身份特征。"
|
||||
"根据@目标角色的身份及系统提供的目标声音设定,重新生成全部相关人声。"
|
||||
"从视频开始到结束,被替换人物的声音必须始终使用同一个目标声音;不得出现原人物声线残留、声音身份中途变化、"
|
||||
"新旧声音混用,或画面角色已经替换但声音仍属于原人物的情况。"
|
||||
"画面中目标角色的嘴部动作必须与重新生成的人声一致,口型、台词开始时间、停顿、语速和情绪表达保持同步。"
|
||||
"与人物身份无关的背景音乐、环境音和必要音效可以保留,但其中不得混入原人物的人声或声纹。"
|
||||
"如保留原视频声音与目标角色声音冲突,必须舍弃原人物声音,以重新生成的目标声音为最高优先级。"
|
||||
)
|
||||
|
||||
# 拆解期间的占位提示词。真正的提示词在 worker 拆完后回写。
|
||||
@@ -380,7 +390,10 @@ def _product_semantic_facts(product: Product) -> dict:
|
||||
"真实卖点": selling_points,
|
||||
"目标用户": product.target_audience.strip(),
|
||||
"规格": product.specs or {},
|
||||
"允许使用场景": (product.specs or {}).get("allowed_use_cases") or (product.specs or {}).get("使用场景") or [],
|
||||
"禁止宣称": (product.specs or {}).get("prohibited_claims") or (product.specs or {}).get("禁止宣称") or [],
|
||||
"资料状态": "完整" if (product.category.strip() and (product.description.strip() or selling_points)) else "不足",
|
||||
"资料不足时策略": "安全展示兜底:只允许拿取、手持展示、放置、携带、包装/材质/外观特写和在桌面陈列;禁止打开、食用、佩戴、插入、涂抹、连接、充电、清洁、宣称功效或任何未确认用途。",
|
||||
}
|
||||
|
||||
|
||||
@@ -398,22 +411,87 @@ def build_product_semantic_remix_messages(*, digest_text: str, product_facts: di
|
||||
f"{facts}\n\n"
|
||||
"【参考视频拆解稿,仅可继承角色关系、环境、镜头语言、剪辑节奏和叙事功能】\n"
|
||||
f"{_digest_for_seedance(digest_text)}\n\n"
|
||||
"输出给视频模型的中文分镜稿:保留【镜头 01】格式和每镜时间、景别、机位、运镜;"
|
||||
"保留参考片的人物、场景、拍摄视角、镜头构图、运镜、剪辑顺序、节奏、情绪和营销结构;将原商品完整替换为目标商品。"
|
||||
"先在内部完成原商品生态、依赖道具、使用接口、动作链与逐镜兼容性检查;不要机械复刻原商品交互。"
|
||||
"原镜用于取出、展示、使用或证明卖点时,保留该镜景别、运镜、时长和表达目的,"
|
||||
"但仅在动作适合目标商品时保留,否则按目标商品结构、用途和卖点重新设计合理动作。"
|
||||
"所有持握、开启、穿戴、使用、安装和展示必须符合真实物理常识:不得尺寸不匹配、虚构配件、商品变形换款变色,"
|
||||
"也不得生成未经商品信息支持的功能。"
|
||||
"如果商品资料状态为‘不足’,不要输出 NEEDS_PRODUCT_FACTS:改用商品资料中的安全展示兜底,"
|
||||
"把原片所有商品使用镜改写为不涉及具体用途的展示镜头。"
|
||||
"镜头数量、编号、时间区间、时长、景别、机位、运镜、切换顺序和剪辑点必须与拆解稿完全一致,绝不能增删、合并或调换镜头。"
|
||||
"参考镜为画内人物对白时,成片必须由画面内人物直接开口说话并口型同步,绝不能改成旁白。"
|
||||
"任何人声都不得说出新商品名称、品牌或旧商品名称,统一用‘这个’、‘它’等自然指代。"
|
||||
"必须同步重写与原商品有关的口播、音效和画面内容;保留原口播的句数、时间位置、语速节奏和情绪,"
|
||||
"但不得保留原商品的名称、功能和卖点。"
|
||||
"最后只输出【SEEDANCE_PROMPT】后的中文分镜稿:保留【镜头 01】格式和每镜时间、时长、景别、机位、运镜、人声方式;"
|
||||
"每镜的画面、人物动作、台词/旁白必须按新商品真实用途重写。"
|
||||
"不要输出分析过程、分类标签或 Markdown。"
|
||||
"禁止输出分析过程、分类标签或 Markdown。"
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def validate_product_semantic_remix(text: str) -> str:
|
||||
"""拒绝模型在事实不足时瞎编,或只给一段泛泛分析而没有可出片分镜。"""
|
||||
"""取出模型的最终分镜稿;任何不可用结果由调用方自动降级,绝不阻断用户任务。"""
|
||||
cleaned = (text or "").strip()
|
||||
if "MISSING_PRODUCT_FACTS" in cleaned:
|
||||
raise ValueError("商品资料不足:请先在商品库补充品类,以及商品描述或至少一条真实卖点后再复刻")
|
||||
if len(cleaned) < 120 or "【镜头" not in cleaned:
|
||||
raise ValueError("商品语义重构结果不完整,请重试")
|
||||
return cleaned
|
||||
# skill 的结构化输出只把最终可出片部分送到 Seedance,避免把原商品分析带进提示词。
|
||||
marker = "【SEEDANCE_PROMPT】"
|
||||
if marker in cleaned:
|
||||
cleaned = cleaned.split(marker, 1)[1].strip()
|
||||
# 这些是模型的“索取资料”占位语,不能作为视频提示词发送;返回空串使上层直接走展示兜底。
|
||||
if any(token in cleaned for token in ("MISSING_PRODUCT_FACTS", "NEEDS_PRODUCT_FACTS", "MANUAL_REVIEW_REQUIRED")):
|
||||
return ""
|
||||
return cleaned if len(cleaned) >= 120 and "【镜头" in cleaned else ""
|
||||
|
||||
|
||||
def build_safe_product_showcase_prompt(*, digest_text: str, product_name: str) -> str:
|
||||
"""资料不足时的确定性兜底:不依赖模型猜用途,也绝不沿用原商品动作。"""
|
||||
from .services import enforce_no_embedded_captions
|
||||
|
||||
header, shots = _parse_digest(digest_text)
|
||||
safe_actions = (
|
||||
"人物从干净桌面拿起@目标商品,保持自然手持展示。",
|
||||
"人物将@目标商品平稳放在桌面上,镜头展示包装轮廓。",
|
||||
"人物缓慢转动@目标商品,展示外观、包装和材质细节。",
|
||||
"镜头靠近@目标商品,拍摄包装与可见结构特写。",
|
||||
)
|
||||
safe_dialogues = (
|
||||
"你看这个,整体的外观和质感都很清楚。",
|
||||
"我把它转过来,细节可以看得更完整。",
|
||||
"从这个角度看,包装和轮廓很直观。",
|
||||
"镜头靠近一点,看看它的材质细节。",
|
||||
)
|
||||
lines = [
|
||||
"【成片规则】",
|
||||
"@目标商品只按参考图展示外观、包装和材质。",
|
||||
"不得出现原商品、原容器、原配件、原动作、原台词、功效或任何未经确认的使用方式。",
|
||||
"镜头数量、时间、时长、景别、机位、运镜和切换顺序必须与参考分镜一致;不得增删、合并、拆分或调换镜头。",
|
||||
"所有人声禁止说出商品名称和品牌。参考镜中人物开口说话时,必须生成画内人物对白和同步口型,禁止改成旁白。",
|
||||
]
|
||||
if header.get("整体风格"):
|
||||
lines.append(f"整体风格:{header['整体风格']}")
|
||||
for index, shot in enumerate(shots, start=1):
|
||||
lines.extend([
|
||||
"",
|
||||
f"【镜头 {index:02d}】",
|
||||
*(f"{field}:{shot[field]}" for field in ("时间", "时长", "景别", "机位", "运镜") if shot.get(field)),
|
||||
f"画面:保持原镜的环境氛围、人物关系与镜头节奏,只出现@目标商品作为展示主体。",
|
||||
f"人物动作:{safe_actions[(index - 1) % len(safe_actions)]}",
|
||||
"人物表情:自然、专注。",
|
||||
f"人声方式:{'画内人物对白' if shot.get('台词/旁白') and not _is_empty_value(shot.get('台词/旁白') or '') else '无'}",
|
||||
f"台词/旁白:{'画内人物对白(人物对镜开口说话,非旁白):' + safe_dialogues[(index - 1) % len(safe_dialogues)] if shot.get('台词/旁白') and not _is_empty_value(shot.get('台词/旁白') or '') else '无'}",
|
||||
])
|
||||
if not shots:
|
||||
lines.extend([
|
||||
"", "【镜头 01】", "时长:5秒", "景别:中近景", "机位:平视", "运镜:缓慢推近",
|
||||
"画面:干净自然的环境中,@目标商品置于桌面中央。",
|
||||
"人物动作:人物自然手持展示@目标商品。",
|
||||
"人物表情:自然、专注。",
|
||||
"人声方式:画内人物对白。",
|
||||
"台词/旁白:画内人物对白(人物对镜开口说话,非旁白):你看这个,外观和包装细节很清楚。",
|
||||
])
|
||||
return enforce_no_embedded_captions("\n".join(lines))
|
||||
|
||||
|
||||
def rewrite_product_semantic_remix(*, task, digest_text: str, product_facts: dict, duration: int, aspect_ratio: str) -> str:
|
||||
@@ -421,23 +499,40 @@ def rewrite_product_semantic_remix(*, task, digest_text: str, product_facts: dic
|
||||
from .video_digest import DIGEST_MAX_TOKENS, resolve_digest_model_config
|
||||
from .services import _collect_extract_text, get_text_provider
|
||||
|
||||
product_name = str(product_facts.get("商品名称") or "")
|
||||
model_config = resolve_digest_model_config()
|
||||
if model_config is None:
|
||||
raise ValueError("商品语义重构模型未配置,请联系管理员")
|
||||
provider = get_text_provider(model_config)
|
||||
text, _payload = _collect_extract_text(
|
||||
provider,
|
||||
model_config,
|
||||
build_product_semantic_remix_messages(
|
||||
logger.warning("商品语义重构模型未配置,回退至安全展示分镜")
|
||||
return build_safe_product_showcase_prompt(
|
||||
digest_text=digest_text,
|
||||
product_facts=product_facts,
|
||||
duration=duration,
|
||||
aspect_ratio=aspect_ratio,
|
||||
),
|
||||
temperature=0.2,
|
||||
extra_body={"max_tokens": DIGEST_MAX_TOKENS},
|
||||
product_name=product_name,
|
||||
)
|
||||
provider = get_text_provider(model_config)
|
||||
messages = build_product_semantic_remix_messages(
|
||||
digest_text=digest_text,
|
||||
product_facts=product_facts,
|
||||
duration=duration,
|
||||
aspect_ratio=aspect_ratio,
|
||||
)
|
||||
last_error: Exception | None = None
|
||||
try:
|
||||
text, _payload = _collect_extract_text(
|
||||
provider, model_config, messages, temperature=0.2,
|
||||
extra_body={"max_tokens": DIGEST_MAX_TOKENS},
|
||||
)
|
||||
prompt = validate_product_semantic_remix(text)
|
||||
if prompt:
|
||||
return prompt
|
||||
last_error = ValueError("模型未提供可用分镜")
|
||||
except Exception as exc:
|
||||
logger.warning("商品语义重构模型调用异常,直接使用展示分镜: %s", exc)
|
||||
last_error = exc
|
||||
# 不再因资料、动作兼容或模型格式而拒绝任务:始终继续走 Seedance。
|
||||
logger.info("商品语义重构未产出可用分镜,自动使用展示分镜 (原因: %s)", last_error)
|
||||
return build_safe_product_showcase_prompt(
|
||||
digest_text=digest_text,
|
||||
product_name=product_name,
|
||||
)
|
||||
return validate_product_semantic_remix(text)
|
||||
|
||||
|
||||
def build_character_replace_prompt(
|
||||
|
||||
Reference in New Issue
Block a user