优化复刻视频脚本和下拉框样式

This commit is contained in:
Azmat@qq.com
2026-08-31 10:59:54 +08:00
parent 284748eeff
commit f59e9d0418
20 changed files with 775 additions and 212 deletions
+125 -6
View File
@@ -9,10 +9,12 @@
"""
from __future__ import annotations
import json
import logging
import re
import uuid
from decimal import Decimal
from pathlib import Path
from django.conf import settings
from django.db import transaction
@@ -80,9 +82,8 @@ def get_inflight_video_replace(team):
)
PRODUCT_PROMPT = (
"按下面这份分镜稿生成视频。镜头数量、每镜时长、景别、机位、运镜、人物、场景、口播全部按稿执行,"
"不要增删镜头、不要改顺序"
"把稿里的原商品换成@目标商品,外观、材质、包装以参考图为准。"
"按下面的商品语义重构分镜生成视频。@目标商品只用于锁定外观、材质与包装。"
"严格遵守每镜的新商品动作、场景与口播;不要恢复参考片原商品、原动作、原卖点或原台词"
)
CHARACTER_PROMPT = (
"按下面这份分镜稿生成视频。镜头数量、每镜时长、景别、机位、运镜、商品、场景、口播全部按稿执行,"
@@ -344,6 +345,99 @@ def _digest_for_seedance(digest_text: str) -> str:
return (head if sep else body).strip()
def _semantic_skill_dir() -> Path:
"""语义重构 skill 必须在 backend/skills 中,确保镜像内也能加载。"""
base = Path(settings.BASE_DIR)
for candidate in (base / "skills", base.parent.parent / "skills"):
skill = candidate / "product-semantic-video-remix" / "SKILL.md"
if skill.is_file():
return skill
return base / "skills" / "product-semantic-video-remix" / "SKILL.md"
def load_product_semantic_remix_skill() -> str:
path = _semantic_skill_dir()
try:
return path.read_text(encoding="utf-8")
except OSError as exc:
raise ValueError("商品语义重构规则未部署,请联系管理员") from exc
def _product_semantic_facts(product: Product) -> dict:
"""只取商品库中已填写的事实,绝不从商品图片猜用途或功效。"""
selling_points = [
{"标题": point.title.strip(), "说明": point.detail.strip()}
for point in product.selling_points.all()
if point.title.strip() or point.detail.strip()
]
return {
"商品名称": product.title.strip(),
"品牌": product.brand.strip(),
"商品品类": product.category.strip(),
"商品描述": product.description.strip(),
"真实卖点": selling_points,
"目标用户": product.target_audience.strip(),
"规格": product.specs or {},
"资料状态": "完整" if (product.category.strip() and (product.description.strip() or selling_points)) else "不足",
}
def build_product_semantic_remix_messages(*, digest_text: str, product_facts: dict, duration: int, aspect_ratio: str) -> list[dict]:
"""参考片只作为结构输入;商品事实和参考片信息严格分栏,防止语义串台。"""
facts = json.dumps(product_facts, ensure_ascii=False, indent=2)
return [
{"role": "system", "content": load_product_semantic_remix_skill()},
{
"role": "user",
"content": (
"请执行商品语义重构,不要直接把参考分镜改几个名词。\n"
f"成片时长:{duration} 秒;画面比例:{aspect_ratio}\n\n"
"【新商品的唯一事实来源】\n"
f"{facts}\n\n"
"【参考视频拆解稿,仅可继承角色关系、环境、镜头语言、剪辑节奏和叙事功能】\n"
f"{_digest_for_seedance(digest_text)}\n\n"
"输出给视频模型的中文分镜稿:保留【镜头 01】格式和每镜时间、景别、机位、运镜;"
"每镜的画面、人物动作、台词/旁白必须按新商品真实用途重写。"
"不要输出分析过程、分类标签或 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
def rewrite_product_semantic_remix(*, task, digest_text: str, product_facts: dict, duration: int, aspect_ratio: str) -> str:
"""Gemini 负责重写商品语义,Seedance 只负责按这份新分镜出片。"""
from .video_digest import DIGEST_MAX_TOKENS, resolve_digest_model_config
from .services import _collect_extract_text, get_text_provider
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(
digest_text=digest_text,
product_facts=product_facts,
duration=duration,
aspect_ratio=aspect_ratio,
),
temperature=0.2,
extra_body={"max_tokens": DIGEST_MAX_TOKENS},
)
return validate_product_semantic_remix(text)
def build_character_replace_prompt(
digest_text: str,
*,
@@ -497,11 +591,18 @@ def submit_video_replace(*, team, user, params: dict):
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)
elif has_model:
subject_name, image_refs, subject_source = _character_library_refs(team, model_id)
product_facts = {}
else:
noun = "商品" if replace_mode == "product" else "角色"
subject_name, image_refs, subject_source = _temporary_image_refs(team, image_ids, noun=noun)
product_facts = {
"商品名称": subject_name,
"资料状态": "不足",
"说明": "临时上传图片只提供外观,未提供品类、用途或真实卖点。",
}
duration = _output_duration(params.get("duration"), video_seconds)
extra = {
@@ -510,6 +611,7 @@ def submit_video_replace(*, team, user, params: dict):
"subject_source": subject_source,
"product_id": str(product_id) if product_id else "",
"model_id": str(model_id) if model_id else "",
"product_facts": product_facts,
}
base_params = {
"mode": "universal",
@@ -643,10 +745,15 @@ def run_replace_digest(task) -> None:
digest, source_seconds=source_seconds, output_seconds=output_seconds,
)
else:
prompt = build_product_replace_prompt(
digest, subject, has_triview=has_triview,
source_seconds=source_seconds, output_seconds=output_seconds,
prompt = rewrite_product_semantic_remix(
task=task,
digest_text=digest,
product_facts=payload.get("product_facts") or {"商品名称": subject, "资料状态": "不足"},
duration=output_seconds,
aspect_ratio=str(payload.get("aspect_ratio") or "9:16"),
)
if has_triview and TRIVIEW_LABEL not in prompt:
prompt = f"{prompt}\n商品各面与材质细节以@{TRIVIEW_LABEL}为准。"
except (VideoDigestError, ValueError) as exc:
_fail_reviewing_task(task, str(exc), error_code="processing_failed")
return
@@ -663,6 +770,7 @@ def run_replace_digest(task) -> None:
next_payload.update(meta)
next_payload["digest_pending"] = False
next_payload["digest_text"] = digest[:32000]
next_payload["semantic_remix"] = replace_mode == "product"
next_payload["prompt"] = prompt
next_payload["references"] = _image_refs(next_payload.get("references") or [])
next_payload.pop("shot_plan", None)
@@ -1583,6 +1691,17 @@ def _product_triview_asset(team, product_id: uuid.UUID):
)
def _product_semantic_facts_for_id(team, product_id: uuid.UUID) -> dict:
product = (
Product.objects.filter(id=product_id, team=team, purged_at__isnull=True, status=Product.Status.ACTIVE)
.prefetch_related("selling_points")
.first()
)
if product is None:
raise ValueError("商品不存在或已被删除")
return _product_semantic_facts(product)
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)