测试视频复刻

This commit is contained in:
Azmat@qq.com
2026-08-27 18:27:08 +08:00
parent b3bf159e08
commit 52c5cdae1d
7 changed files with 299 additions and 106 deletions
+71 -12
View File
@@ -40,6 +40,9 @@ FEATURE = "video_replace"
REPLACE_MODES = {"product", "character"}
MAX_IMAGES = 9
LEGACY_PROMPT_PREFIX = "[视频复刻]"
# 商品三视图在商品库是 standalone Asset(metadata.view=three_view + product_id),
# 和项目内的 BaseAssetGroup 是两套存储,这里直接取商品库那份。
TRIVIEW_LABEL = "目标商品三视图"
REVIEW_UNAVAILABLE = "素材审核服务暂不可用,请稍后重试"
REVIEW_FAILED = "参考素材未通过真人合规审核,请更换视频或图片后重试"
REVIEW_SUBMIT_FAILED = "素材提交审核失败,请稍后重试"
@@ -57,10 +60,17 @@ PRODUCT_DIGEST_TAIL = (
"1. 严格按上面分镜稿的镜头顺序、时间分配、景别、机位、运镜和剪辑节奏还原全片,不要自行增删镜头。\n"
"2. 分镜稿里出现的原商品,全部替换成 @目标商品;商品的外形、颜色、材质、包装和标识必须与参考图完全一致,"
"不要改造型、不要改配色、不要凭空补细节。\n"
"{triview}"
"3. 人物、场景、光线、色调、动作和表情按分镜稿保持不变,只换商品。\n"
"4. 台词/旁白按分镜稿逐字念出;原文提到旧商品名称的地方,改说「{subject}」。\n"
"5. 分镜稿里的「字幕」栏只是对原片的记录,不要把这些文字画到画面上。"
)
# 商品库里有三视图时才追加这一条(没有就不提,免得模型去找一张不存在的图)。
PRODUCT_TRIVIEW_NOTE = (
" · @目标商品三视图 是这件商品的白底多角度图,用它确认商品的立体结构、比例和各个面的细节;"
"镜头转到任何角度,商品都要和三视图对得上。\n"
)
# 拆解期间的占位提示词。真正的提示词在 worker 拆完后回写。
DIGEST_PENDING_PROMPT = "正在提炼参考视频的分镜稿…"
@@ -97,7 +107,7 @@ 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) -> str:
def build_product_replace_prompt(digest_text: str, subject_name: str, *, has_triview: bool = False) -> str:
"""分镜稿 + 商品替换要求 → 交给 Seedance 的完整提示词。
@目标商品 必须与 references 里第一张商品图的 label 对上,否则 build_content_items
@@ -109,7 +119,10 @@ def build_product_replace_prompt(digest_text: str, subject_name: str) -> str:
if not body:
raise ValueError("参考视频拆解结果为空,请重试")
subject = (subject_name or "").strip() or "目标商品"
tail = PRODUCT_DIGEST_TAIL.format(subject=subject)
tail = PRODUCT_DIGEST_TAIL.format(
subject=subject,
triview=PRODUCT_TRIVIEW_NOTE if has_triview else "",
)
return enforce_no_embedded_captions(f"{PRODUCT_DIGEST_HEAD}\n\n{body}\n\n{tail}")
@@ -210,7 +223,17 @@ def submit_video_replace(*, team, user, params: dict):
}
if replace_mode == "product":
# 商品复刻:参考视频只喂给提炼模型,不进火山 references(所以也不用送火山审核)。
# 商品复刻:参考视频只喂给提炼模型,不进火山 references(所以参考视频本身不再送火山审核)。
# 两个「一定会失败」的前提在这里就查掉,别让用户白等一轮提炼才看到报错:
from .video_digest import resolve_digest_model_config
if resolve_digest_model_config() is None:
raise ValueError("视频提炼模型未配置,请联系管理员")
# 商品图仍要过审才能当生成参考。提前送审 → 审核和提炼并行跑,明确不过审的直接 400。
review_state = _ensure_replace_refs_reviewed(team, image_refs)
if review_state == "failed":
raise ValueError(REVIEW_FAILED)
image_refs = _refresh_replace_refs(team, image_refs)
# 先秒回一个 CREATED 任务,拆解这种慢活(Gemini 半分钟起)交给 worker。
extra.update({
"digest_source_asset_id": str(video.id),
@@ -313,18 +336,23 @@ def run_replace_digest(task) -> None:
task.team, uuid.UUID(str(payload.get("digest_source_asset_id") or "")), "参考视频"
)
except (ValueError, TypeError):
_fail_reviewing_task(task, "参考视频已失效,请重新上传")
_fail_reviewing_task(task, "参考视频已失效,请重新上传", error_code="asset_unavailable")
return
try:
digest, meta = digest_asset_video(asset=asset, task=task)
prompt = build_product_replace_prompt(digest, str(payload.get("subject_name") or ""))
has_triview = any(
(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
)
except (VideoDigestError, ValueError) as exc:
_fail_reviewing_task(task, str(exc))
_fail_reviewing_task(task, str(exc), error_code="processing_failed")
return
except Exception as exc: # noqa: BLE001 — 拆解任何异常都要把任务收尾,别留 CREATED 僵尸
logger.exception("video replace digest failed for %s", task.id)
_fail_reviewing_task(task, f"参考视频拆解失败:{exc}")
_fail_reviewing_task(task, f"参考视频拆解失败:{exc}", error_code="processing_failed")
return
with transaction.atomic():
@@ -354,10 +382,20 @@ def _legacy_replace_mode(prompt: str) -> str:
return "character" if prompt.startswith("[视频复刻·角色]") else "product"
def _fail_reviewing_task(task, message: str):
def _fail_reviewing_task(task, message: str, *, error_code: str = ""):
"""收尾一个还没提交火山的任务。
``_fail_pending_free_video`` 把错误码写死成 content_rejected —— 那是审核不通过的语义,
前端会渲染成「内容未通过生成审核」且不可重试。拆解失败/素材丢失不是那回事,
这里按实际原因改码,否则模型抖一下会被误报成合规问题,用户白白去换素材。
"""
from .free_video import _fail_pending_free_video
return _fail_pending_free_video(task, message)
task = _fail_pending_free_video(task, message)
if error_code and task.status == AITask.Status.FAILED and task.error_code != error_code:
task.error_code = error_code
task.save(update_fields=["error_code", "updated_at"])
return task
def _enqueue_replace_review_poll(task):
@@ -701,6 +739,21 @@ def _library_image_ref(asset: Asset, *, team, label: str) -> dict:
}
def _product_triview_asset(team, product_id: uuid.UUID):
"""商品库里这个商品的三视图(白底多角度单张 16:9)。没有就返回 None,不挡生成。"""
return (
Asset.objects.filter(
team=team,
is_deleted=False,
purged_at__isnull=True,
metadata__product_id=str(product_id),
metadata__view="three_view",
)
.order_by("-created_at")
.first()
)
def _product_library_refs(team, product_id: uuid.UUID) -> tuple[str, list, str]:
product = (
Product.objects.filter(id=product_id, team=team, purged_at__isnull=True, status=Product.Status.ACTIVE)
@@ -710,21 +763,27 @@ def _product_library_refs(team, product_id: uuid.UUID) -> tuple[str, list, str]:
)
if product is None:
raise ValueError("商品不存在或已被删除")
triview = _product_triview_asset(team, product_id)
# 三视图也占一张参考图的名额,商品图要给它让位,否则火山那边超 9 张直接拒。
image_budget = MAX_IMAGES - 1 if triview is not None else MAX_IMAGES
assets = []
seen = set()
seen = {triview.id} if triview is not None else set()
for image in product.images.all():
asset = image.asset
if asset is None or asset.id in seen or asset.is_deleted:
continue
seen.add(asset.id)
assets.append(asset)
if len(assets) >= MAX_IMAGES:
if len(assets) >= image_budget:
break
if not assets and product.cover_asset_id and not product.cover_asset.is_deleted:
assets.append(product.cover_asset)
if not assets:
if not assets and triview is None:
raise ValueError("这个商品还没有可用图片")
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:
# 放最后:@目标商品 仍指向第一张实拍图,三视图作为「各面长什么样」的补充证据。
refs.append(_library_image_ref(triview, team=team, label=TRIVIEW_LABEL))
return product.title, refs, "library"