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:
seaislee1209
2026-06-17 22:17:47 +08:00
co-authored by Claude Opus 4.8
parent 016c859004
commit edaa314261
+97 -63
View File
@@ -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,