feat(core): 故事板/视频提示词改用户钦定 @图N 模板拼装
故事板:撤『分镜表』回『一镜=一张导演故事板』;refs 版出【设定】@图N是角色/商品/场景 + 导演故事板指令 + 【分镜脚本】(本段约X秒) + 该镜脚本;_storyboard_reference_images 按 角色→商品→场景 排序,@图N 与 image_edit 传图顺序锁死。视频:_video_reference_images 收齐 角色/场景/商品 基础资产 + 本镜故事板帧(四类带类型,角色→场景→商品→分镜图);build_video_segment_prompt 出【设定】@图N…【分镜】根据@图N分镜图…【脚本】该镜脚本;submit_video_segment 传图顺序与 @图N 对齐。新增 _segment_script_text 复用。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
016c859004
commit
edaa314261
@@ -676,48 +676,59 @@ def _scene_context(project) -> str:
|
||||
return " · ".join(parts)
|
||||
|
||||
|
||||
def build_storyboard_frame_prompt(project, version, segment) -> str:
|
||||
"""单场故事板提示词:把这一「场」拆成多个分镜,排成一张竖屏【分镜表】。
|
||||
用户要的结构是「一场 = 一个故事板,内含多个分镜图(不同景别/运镜)」,而非一场一张大图。"""
|
||||
visual = (segment.visual_prompt or segment.narration or "").strip()
|
||||
dur = segment.duration_seconds or 15
|
||||
sub = max(2, min(4, round(dur / 5))) # 按本场时长估分镜数:15s≈3 格
|
||||
lines = [
|
||||
_scene_context(project),
|
||||
f"生成一张竖屏【分镜故事板表】:把第 {segment.sort_order + 1} 场(约 {dur} 秒)拆成 {sub} 个分镜,"
|
||||
f"从上到下纵向排成 {sub} 个等宽分格,格与格之间用细线或留白分隔,每格角落标注分镜序号、景别与时长。",
|
||||
]
|
||||
def _segment_script_text(segment) -> str:
|
||||
"""本镜脚本文本(画面 + 口播/旁白 + 商品露出),拼进故事板/视频提示词的【分镜脚本】。"""
|
||||
parts = []
|
||||
visual = (segment.visual_prompt or "").strip()
|
||||
if visual:
|
||||
lines.append(f"本场画面:{visual}")
|
||||
if segment.narration:
|
||||
lines.append(f"台词/口播:{segment.narration.strip()}")
|
||||
if segment.product_exposure:
|
||||
lines.append(f"商品露出方式:{segment.product_exposure.strip()}")
|
||||
lines.append(
|
||||
"各分镜要体现不同景别与运镜(如 中景固定 → 特写微距 → 全景跟拍),镜头连贯,"
|
||||
"同一人物保持同一张脸、同一商品保持外观与配色一致。"
|
||||
)
|
||||
parts.append(f"画面:{visual}")
|
||||
narration = (segment.narration or "").strip()
|
||||
if narration:
|
||||
parts.append(f"口播/旁白:{narration}")
|
||||
if getattr(segment, "product_exposure", ""):
|
||||
parts.append(f"商品露出:{segment.product_exposure.strip()}")
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
def build_storyboard_frame_prompt(project, version, segment) -> str:
|
||||
"""单镜导演故事板提示词(一镜 = 一张导演故事板,用户钦定结构)。无参考图时的文本版。"""
|
||||
dur = segment.duration_seconds or 15
|
||||
lines = [
|
||||
"根据以下脚本生成一个导演故事板,用于指导 seedance 的视频生成。",
|
||||
_scene_context(project),
|
||||
f"【分镜脚本】(本段时长约 {dur} 秒)",
|
||||
_segment_script_text(segment) or f"第 {segment.sort_order + 1} 镜",
|
||||
]
|
||||
if version.prompt:
|
||||
lines.append(version.prompt.strip())
|
||||
lines.append("真实清新、生活化的电商带货分镜表,竖屏 9:16,排版工整,画面清晰,可直接指导视频生成。")
|
||||
lines.append("电商竖屏 9:16 导演故事板,真实清新、生活化,画面清晰,可直接指导视频生成。")
|
||||
return "\n".join(line for line in lines if line)
|
||||
|
||||
|
||||
def build_video_segment_prompt(project, video_segment, scene, user_prompt: str) -> str:
|
||||
"""单段视频提示词:把本镜旁白 + 画面 + 风格锚点织进去,让每个视频片段跟住对应脚本/故事板。"""
|
||||
lines = [_scene_context(project)]
|
||||
if scene is not None:
|
||||
if scene.narration:
|
||||
lines.append(f"旁白:{scene.narration.strip()}")
|
||||
visual = (scene.visual_prompt or scene.narration or "").strip()
|
||||
if visual:
|
||||
lines.append(f"画面:{visual}")
|
||||
if user_prompt:
|
||||
lines.append(user_prompt.strip())
|
||||
lines.append(
|
||||
f"第 {video_segment.sort_order + 1} 段 · {video_segment.target_duration_seconds}s · "
|
||||
"9:16 竖屏电商带货短视频,镜头稳定,商品露出清晰,节奏有转化感"
|
||||
)
|
||||
def build_video_segment_prompt(project, video_segment, scene, refs, user_prompt: str = "") -> str:
|
||||
"""单段视频提示词(用户钦定 @图N 格式):
|
||||
【设定】@图N 点名 角色/场景/商品;【分镜】根据 @图(分镜图) 生成;【脚本】本镜脚本。
|
||||
refs 顺序与传给 seedance 的 reference_images 一致(@图N 对齐不错位)。"""
|
||||
product_name = (getattr(project.product, "title", "") or "商品").strip()
|
||||
setup_parts = []
|
||||
storyboard_idx = None
|
||||
for i, r in enumerate(refs or []):
|
||||
n = i + 1
|
||||
if r.get("type") == "storyboard":
|
||||
storyboard_idx = n
|
||||
else:
|
||||
setup_parts.append(f"@图{n}是{r.get('label') or ''}({_ENTITY_TYPE_CN.get(r.get('type'), '参考')})")
|
||||
lines = []
|
||||
if setup_parts:
|
||||
lines.append("【设定】" + ",".join(setup_parts) + "。")
|
||||
if storyboard_idx is not None:
|
||||
lines.append(f"【分镜】根据@图{storyboard_idx}分镜图生成「{product_name}」短视频。")
|
||||
script_text = _segment_script_text(scene) if scene is not None else ""
|
||||
extra = (user_prompt or "").strip()
|
||||
if extra:
|
||||
script_text = (script_text + "\n" + extra).strip() if script_text else extra
|
||||
lines.append("【脚本】" + (script_text or f"第 {video_segment.sort_order + 1} 段"))
|
||||
lines.append(f"{video_segment.target_duration_seconds}s · 9:16 竖屏电商带货短视频,镜头稳定,商品露出清晰,节奏有转化感。")
|
||||
return "\n".join(line for line in lines if line)
|
||||
|
||||
|
||||
@@ -780,22 +791,32 @@ def _storyboard_reference_images(project, segment) -> list[dict]:
|
||||
url = _asset_preview_url(pg.adopted_asset)
|
||||
if url:
|
||||
out.append({"url": url, "label": "商品", "type": "product"})
|
||||
# 规范 @图N 顺序:角色 → 商品 → 场景(与下游 image_edit 传图顺序一致,标注不错位)
|
||||
_ord = {"character": 0, "product": 1, "scene": 2}
|
||||
out.sort(key=lambda r: _ord.get(r.get("type"), 9))
|
||||
return out
|
||||
|
||||
|
||||
def build_storyboard_frame_prompt_refs(project, version, segment, refs: list[dict]) -> str:
|
||||
"""参考图合成版故事板提示词:在基础提示词上点名每张参考图,要求锁脸/锁商品外观。"""
|
||||
base = build_storyboard_frame_prompt(project, version, segment)
|
||||
"""参考图合成版(用户钦定格式):顶部 @图N 点名每张参考图(角色/商品/场景),再给导演故事板指令 + 分镜脚本。
|
||||
@图N 顺序与传给 gpt-image-2 的参考图顺序严格一致(refs 即 image_edit 的 images 顺序)。"""
|
||||
if not refs:
|
||||
return base
|
||||
ref_lines = ";".join(
|
||||
f"参考图{i + 1}={r['label']}({_ENTITY_TYPE_CN.get(r.get('type'), '参考')})" for i, r in enumerate(refs)
|
||||
)
|
||||
return (
|
||||
f"{base}\n参考图对应:{ref_lines}。"
|
||||
"请严格保持各参考图中角色的同一张脸、同一商品的外观与配色,按本场各分镜画面重新构图,"
|
||||
"合成为一张包含多个分镜格的竖屏分镜表(每格一个分镜画面)。"
|
||||
return build_storyboard_frame_prompt(project, version, segment)
|
||||
setup = ",".join(
|
||||
f"@图{i + 1}是{r['label']}({_ENTITY_TYPE_CN.get(r.get('type'), '参考')})" for i, r in enumerate(refs)
|
||||
)
|
||||
dur = segment.duration_seconds or 15
|
||||
lines = [
|
||||
f"【设定】{setup}。",
|
||||
"根据以下脚本生成一个导演故事板,用于指导 seedance 的视频生成。",
|
||||
_scene_context(project),
|
||||
f"【分镜脚本】(本段时长约 {dur} 秒)",
|
||||
_segment_script_text(segment) or f"第 {segment.sort_order + 1} 镜",
|
||||
"请严格保持各参考图中角色的同一张脸、同一商品的外观与配色;电商竖屏 9:16 导演故事板,一镜一图,画面清晰,可直接指导视频生成。",
|
||||
]
|
||||
if version.prompt:
|
||||
lines.append(version.prompt.strip())
|
||||
return "\n".join(line for line in lines if line)
|
||||
|
||||
|
||||
def _storyboard_frame_worker(task_id, version_id, segment_id, user_id) -> None:
|
||||
@@ -981,8 +1002,21 @@ def _asset_preview_url(asset) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
def _video_reference_images(project, video_segment) -> list[str]:
|
||||
"""为本视频段挑一张视觉参考图:优先本镜故事板帧,兜底已采用商品基础资产。"""
|
||||
def _video_reference_images(project, video_segment) -> list[dict]:
|
||||
"""视频参考图(带类型,供 @图N):角色/场景/商品 基础资产 + 本镜故事板帧。
|
||||
顺序:角色 → 场景 → 商品 → 分镜图(与用户钦定 @图1角色@图2场景@图3商品@图4分镜图 一致)。
|
||||
返回 [{url,label,type}];都取不到时兜底商品图。"""
|
||||
out: list[dict] = []
|
||||
scene = None
|
||||
adopted_script = project.script_versions.filter(is_adopted=True).prefetch_related("segments").first()
|
||||
if adopted_script is not None:
|
||||
scene = adopted_script.segments.filter(sort_order=video_segment.sort_order).first()
|
||||
if scene is not None:
|
||||
refs = list(_storyboard_reference_images(project, scene)) # 角色/商品/场景 实体图
|
||||
_vord = {"character": 0, "scene": 1, "product": 2}
|
||||
refs.sort(key=lambda r: _vord.get(r.get("type"), 9))
|
||||
out = refs
|
||||
# 末位追加本镜故事板帧(@图N 末位 = 分镜图)
|
||||
version = (
|
||||
project.storyboard_versions.filter(is_adopted=True).order_by("-created_at").first()
|
||||
or project.storyboard_versions.order_by("-created_at").first()
|
||||
@@ -995,17 +1029,17 @@ def _video_reference_images(project, video_segment) -> list[str]:
|
||||
if frame is not None:
|
||||
url = _asset_preview_url(frame.asset)
|
||||
if url:
|
||||
return [url]
|
||||
product_group = (
|
||||
project.base_asset_groups.filter(kind=BaseAssetGroup.Kind.PRODUCT, adopted_asset__isnull=False)
|
||||
.order_by("-created_at")
|
||||
.first()
|
||||
)
|
||||
if product_group is not None:
|
||||
url = _asset_preview_url(product_group.adopted_asset)
|
||||
if url:
|
||||
return [url]
|
||||
return []
|
||||
out.append({"url": url, "label": "分镜图", "type": "storyboard"})
|
||||
if not out:
|
||||
product_group = (
|
||||
project.base_asset_groups.filter(kind=BaseAssetGroup.Kind.PRODUCT, adopted_asset__isnull=False)
|
||||
.order_by("-created_at").first()
|
||||
)
|
||||
if product_group is not None:
|
||||
url = _asset_preview_url(product_group.adopted_asset)
|
||||
if url:
|
||||
out.append({"url": url, "label": "商品", "type": "product"})
|
||||
return out
|
||||
|
||||
|
||||
def submit_video_segment(*, video_segment: VideoSegment, user, prompt: str) -> VideoSegmentVersion | None:
|
||||
@@ -1022,10 +1056,10 @@ def submit_video_segment(*, video_segment: VideoSegment, user, prompt: str) -> V
|
||||
if scene is not None and video_segment.script_segment_id != scene.id:
|
||||
video_segment.script_segment = scene
|
||||
video_segment.save(update_fields=["script_segment", "updated_at"])
|
||||
final_prompt = build_video_segment_prompt(project, video_segment, scene, prompt)
|
||||
|
||||
# 参考图:优先用本镜故事板帧,其次商品/人物基础资产,给视频做视觉锚点(衔接故事板→视频)。
|
||||
reference_images = _video_reference_images(project, video_segment)
|
||||
# 参考图(带类型):角色/场景/商品 基础资产 + 本镜故事板帧;@图N 提示词与传图顺序严格对齐。
|
||||
refs = _video_reference_images(project, video_segment)
|
||||
reference_images = [r["url"] for r in refs]
|
||||
final_prompt = build_video_segment_prompt(project, video_segment, scene, refs, prompt)
|
||||
|
||||
task = create_ai_task(
|
||||
project=project,
|
||||
|
||||
Reference in New Issue
Block a user