完善视频提炼和视频复刻完善提交测试
This commit is contained in:
@@ -43,10 +43,38 @@ LEGACY_PROMPT_PREFIX = "[视频复刻]"
|
||||
# 商品三视图在商品库是 standalone Asset(metadata.view=three_view + product_id),
|
||||
# 和项目内的 BaseAssetGroup 是两套存储,这里直接取商品库那份。
|
||||
TRIVIEW_LABEL = "目标商品三视图"
|
||||
# 商品复刻的参考视频不进火山、只喂提炼模型,所以不受火山 15 秒参考视频限制。
|
||||
# 半秒容差同 media_probe 的老规矩(60 秒成片常被探成 60.02)。角色复刻仍是 15 秒。
|
||||
PRODUCT_REF_DURATION_MAX = 60.5
|
||||
REVIEW_UNAVAILABLE = "素材审核服务暂不可用,请稍后重试"
|
||||
REVIEW_FAILED = "参考素材未通过真人合规审核,请更换视频或图片后重试"
|
||||
REVIEW_SUBMIT_FAILED = "素材提交审核失败,请稍后重试"
|
||||
|
||||
|
||||
class VideoReplaceInProgress(ValueError):
|
||||
"""本团队已有复刻在跑。视图转 409,并把在跑的那条带回去让前端接上。"""
|
||||
|
||||
def __init__(self, task):
|
||||
super().__init__("已有一个视频正在复刻中,完成或取消后才能再提交")
|
||||
self.task = task
|
||||
|
||||
|
||||
def get_inflight_video_replace(team):
|
||||
"""本团队正在跑的复刻任务(含审核中 / 提炼中)。没有返回 None。"""
|
||||
_reap_stale_free_video_tasks(team=team)
|
||||
return (
|
||||
AITask.objects.filter(
|
||||
team=team,
|
||||
task_type=AITask.Type.FREE_VIDEO,
|
||||
status__in=IN_FLIGHT_STATUSES,
|
||||
is_deleted=False,
|
||||
purged_at__isnull=True,
|
||||
)
|
||||
.filter(video_replace_q())
|
||||
.order_by("-created_at")
|
||||
.first()
|
||||
)
|
||||
|
||||
# 商品复刻不再把参考视频直接丢给火山(实测换不动商品,出片跑偏),改走两步:
|
||||
# 先用「提炼提示词」那一整套(同一份 SKILL.md + 同一个 Gemini 3.1 Pro)把参考视频拆成
|
||||
# 中文分镜稿,再把分镜稿当导演脚本、连同商品图一起交给 Seedance。
|
||||
@@ -62,8 +90,22 @@ PRODUCT_DIGEST_TAIL = (
|
||||
"不要改造型、不要改配色、不要凭空补细节。\n"
|
||||
"{triview}"
|
||||
"3. 人物、场景、光线、色调、动作和表情按分镜稿保持不变,只换商品。\n"
|
||||
" · 全片的人物必须自始至终是同一个人:五官、发型、体态和服装在所有镜头里保持一致,"
|
||||
"不要中途换脸、换人或换装。\n"
|
||||
" · 参考图里如果有人物(例如服饰类的模特图、真人手持商品的图),画面中的人物就以那张参考图为准,"
|
||||
"五官、发型、体态和服装必须与之一致;参考图里只有商品、没有人物时,就按分镜稿描述的人物去演,"
|
||||
"不要从商品图上凭空套一张脸。\n"
|
||||
"4. 台词/旁白按分镜稿逐字念出;原文提到旧商品名称的地方,改说「{subject}」。\n"
|
||||
"5. 分镜稿里的「字幕」栏只是对原片的记录,不要把这些文字画到画面上。"
|
||||
"{condense}"
|
||||
)
|
||||
|
||||
# 参考视频比成片长时才追加(火山单次出片上限 15 秒,60 秒参考稿必须压缩改编,
|
||||
# 否则模型会照着分镜稿从头拍、拍到 15 秒戛然而止)。
|
||||
PRODUCT_CONDENSE_NOTE = (
|
||||
"\n6. 参考视频约 {source} 秒,而本次成片只有 {output} 秒:请按分镜稿的结构和节奏**压缩改编**,"
|
||||
"保留主线、最关键的卖点镜头和结尾收束,可以合并或省略次要镜头;"
|
||||
"不要只拍开头几镜就停,也不要把画面加速成快放。"
|
||||
)
|
||||
# 商品库里有三视图时才追加这一条(没有就不提,免得模型去找一张不存在的图)。
|
||||
PRODUCT_TRIVIEW_NOTE = (
|
||||
@@ -107,7 +149,14 @@ def compact_digest_for_video(digest_text: str) -> str:
|
||||
return "\n".join(line for line in lines if line.strip()).strip()
|
||||
|
||||
|
||||
def build_product_replace_prompt(digest_text: str, subject_name: str, *, has_triview: bool = False) -> str:
|
||||
def build_product_replace_prompt(
|
||||
digest_text: str,
|
||||
subject_name: str,
|
||||
*,
|
||||
has_triview: bool = False,
|
||||
source_seconds: float = 0,
|
||||
output_seconds: int = 0,
|
||||
) -> str:
|
||||
"""分镜稿 + 商品替换要求 → 交给 Seedance 的完整提示词。
|
||||
|
||||
@目标商品 必须与 references 里第一张商品图的 label 对上,否则 build_content_items
|
||||
@@ -119,9 +168,18 @@ def build_product_replace_prompt(digest_text: str, subject_name: str, *, has_tri
|
||||
if not body:
|
||||
raise ValueError("参考视频拆解结果为空,请重试")
|
||||
subject = (subject_name or "").strip() or "目标商品"
|
||||
source = int(round(float(source_seconds or 0)))
|
||||
output = int(output_seconds or 0)
|
||||
# 差 3 秒以内不啰嗦,模型自己会收;差得多才明确要求压缩改编。
|
||||
condense = (
|
||||
PRODUCT_CONDENSE_NOTE.format(source=source, output=output)
|
||||
if source and output and source - output >= 3
|
||||
else ""
|
||||
)
|
||||
tail = PRODUCT_DIGEST_TAIL.format(
|
||||
subject=subject,
|
||||
triview=PRODUCT_TRIVIEW_NOTE if has_triview else "",
|
||||
condense=condense,
|
||||
)
|
||||
return enforce_no_embedded_captions(f"{PRODUCT_DIGEST_HEAD}\n\n{body}\n\n{tail}")
|
||||
|
||||
@@ -190,9 +248,20 @@ def submit_video_replace(*, team, user, params: dict):
|
||||
if not has_model and not has_temp:
|
||||
raise ValueError("请选择角色或上传角色参考图")
|
||||
|
||||
# 单飞闸:一个团队同时只允许一条复刻在跑。刷新页面时前端拉状态有空档,
|
||||
# 用户容易以为「没任务」再点一次,重复扣费。_reap_stale_free_video_tasks 会先回收
|
||||
# 死任务(CREATED 超 16 分钟等),所以不会被永久锁住。
|
||||
running = get_inflight_video_replace(team)
|
||||
if running is not None:
|
||||
raise VideoReplaceInProgress(running)
|
||||
|
||||
video = _team_asset(team, params.get("video_asset_id"), kind=Asset.Type.VIDEO, label="参考视频")
|
||||
video_seconds = _asset_duration_seconds(video)
|
||||
if video_seconds > REF_DURATION_MAX:
|
||||
if replace_mode == "product":
|
||||
if video_seconds > PRODUCT_REF_DURATION_MAX:
|
||||
raise ValueError("参考视频不能超过 60 秒,请剪短后重试")
|
||||
elif video_seconds > REF_DURATION_MAX:
|
||||
# 角色复刻把参考视频直传火山,15 秒是火山那边的硬限制,不能跟着放宽。
|
||||
raise ValueError("参考视频不能超过 15 秒,请剪短后重试")
|
||||
|
||||
if has_product:
|
||||
@@ -345,7 +414,11 @@ def run_replace_digest(task) -> None:
|
||||
(ref or {}).get("label") == TRIVIEW_LABEL for ref in (payload.get("references") or [])
|
||||
)
|
||||
prompt = build_product_replace_prompt(
|
||||
digest, str(payload.get("subject_name") or ""), has_triview=has_triview
|
||||
digest,
|
||||
str(payload.get("subject_name") or ""),
|
||||
has_triview=has_triview,
|
||||
source_seconds=float(payload.get("digest_source_duration") or 0),
|
||||
output_seconds=int(payload.get("duration") or 0),
|
||||
)
|
||||
except (VideoDigestError, ValueError) as exc:
|
||||
_fail_reviewing_task(task, str(exc), error_code="processing_failed")
|
||||
@@ -776,10 +849,15 @@ def _product_library_refs(team, product_id: uuid.UUID) -> tuple[str, list, str]:
|
||||
assets.append(asset)
|
||||
if len(assets) >= image_budget:
|
||||
break
|
||||
if not assets and product.cover_asset_id and not product.cover_asset.is_deleted:
|
||||
if not assets and product.cover_asset_id and product.cover_asset_id not in seen and not product.cover_asset.is_deleted:
|
||||
assets.append(product.cover_asset)
|
||||
if not assets and triview is None:
|
||||
raise ValueError("这个商品还没有可用图片")
|
||||
if not assets:
|
||||
# 只有三视图、没有任何实拍图时,把三视图顶上来当 @目标商品。
|
||||
# 否则提示词里的 @目标商品 找不到同名 label,火山拿到的是一句没有指代的字面量。
|
||||
if triview is None:
|
||||
raise ValueError("这个商品还没有可用图片")
|
||||
assets = [triview]
|
||||
triview = None
|
||||
refs = [_library_image_ref(asset, team=team, label="目标商品" if index == 0 else f"目标商品{index + 1}") for index, asset in enumerate(assets)]
|
||||
if triview is not None:
|
||||
# 放最后:@目标商品 仍指向第一张实拍图,三视图作为「各面长什么样」的补充证据。
|
||||
|
||||
Reference in New Issue
Block a user