feat(core): 故事板改为「一场=分镜表」+ 确认故事板按钮气泡引导

- ai/services.py: 故事板单帧提示词改为按场拆多分镜、纵向排成竖屏分镜表(不同景别/运镜),并锁人物脸/商品一致性
- pipeline.tsx: 底栏统一「确认故事板」,未生成时点击弹气泡引导先生成(不再假装跳过)
- pipeline-page.css: 配套 sb-confirm-pop 气泡样式
- 附测试截图与流程文档/perf 产物

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
zyc
2026-06-17 18:32:06 +08:00
co-authored by Claude Opus 4.8
parent 2f6d3f5823
commit d959543d43
20 changed files with 456 additions and 11 deletions
+8
View File
@@ -583,6 +583,14 @@
.tri-missing-pop .pop-tip { font-size: 12px; line-height: 1.5; color: var(--black-alpha-56); }
.tri-missing-pop b { color: var(--heat); }
/* ── 底栏「确认故事板」点击气泡:没生成故事板时点确认 → 朝上弹提示(复用 pop 视觉) ── */
.sb-confirm-wrap { position: relative; display: inline-flex; }
.sb-confirm-pop { position: absolute; bottom: calc(100% + 10px); right: 0; width: 248px; padding: 12px 14px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); box-shadow: 0 12px 32px rgba(0,0,0,.14); display: flex; flex-direction: column; gap: 6px; z-index: 30; animation: sb-confirm-in var(--t-base); }
.sb-confirm-pop .pop-h { display: inline-flex; align-items: center; gap: 5px; font-family: var(--font-mono); font-size: 12px; letter-spacing: .04em; color: var(--heat); }
.sb-confirm-pop .pop-body { font-size: 12px; line-height: 1.55; color: var(--accent-black); }
.sb-confirm-pop b { color: var(--heat); }
@keyframes sb-confirm-in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
/* ── 行39 · 三视图选择弹窗 ── */
.tri-modal { width: min(720px, 100%); }
.tri-modal .tri-modal-tip { font-size: 13px; line-height: 1.55; color: var(--black-alpha-72); padding: 10px 12px; background: var(--heat-12); border: 1px solid var(--heat-20); border-radius: var(--r-md); margin-bottom: 16px; }
+21 -7
View File
@@ -667,6 +667,13 @@ export function PipelinePage(props: {
// 让用户一眼看出「这里本该有几张」,出图后逐个填真图。无采用版脚本则为 0(显示「暂无」)。
const sbAdoptedScript = scripts.find((s) => s.is_adopted) || null;
const sbExpectedShots = sbAdoptedScript ? (sbAdoptedScript.segments?.length ?? 0) : 0;
// 没生成故事板时点「确认故事板」→ 弹气泡提示先去生成(3.5s 自动收起)
const [sbConfirmHint, setSbConfirmHint] = useState(false);
useEffect(() => {
if (!sbConfirmHint) return;
const t = window.setTimeout(() => setSbConfirmHint(false), 3500);
return () => window.clearTimeout(t);
}, [sbConfirmHint]);
// ── Stage 4:视频片段(adopted_asset 缩略图 + 状态 pill + 时长)──
const segments = [...(project.video_segments ?? [])].sort((a, b) => a.sort_order - b.sort_order);
@@ -2601,7 +2608,7 @@ export function PipelinePage(props: {
: <span className="pill neutral"><span className="dot"></span></span>)
: <span className="pill neutral"><span className="dot"></span></span>}
</div>
<div className="muted-2" style={{ fontSize: "12px", lineHeight: 1.55, marginBottom: "10px" }}> image-2 , + </div>
<div className="muted-2" style={{ fontSize: "12px", lineHeight: 1.55, marginBottom: "10px" }}> · image-2 (/)</div>
<div className="sb-rerun-note">
<span className="warn-ic" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" /><path d="M12 9v4M12 17h.01" /></svg>
@@ -2656,12 +2663,19 @@ export function PipelinePage(props: {
<div className="info"><span className="mono">[ image-2 · {sbFrames.length ? `${sbFrames.length}` : sbExpectedShots ? `待生成 ${sbExpectedShots}` : "0 场"} · , ]</span></div>
<div className="hstack">
<button className="btn" type="button" onClick={() => goStage(2)}><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M19 12H5M12 19l-7-7 7-7" /></svg> </button>
{adoptedStoryboard ? (
<button className="btn btn-primary btn-lg" type="button" onClick={() => goStage(4)}>, <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M5 12h14M12 5l7 7-7 7" /></svg></button>
) : (
/* 没有故事板时不再假装「确认故事板」:走真实 skip-storyboard,把项目阶段如实推进到视频 */
<button className="btn btn-primary btn-lg" type="button" disabled={loading} onClick={async () => { await onSkipStoryboard(); goStage(4); }}>, <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M5 12h14M12 5l7 7-7 7" /></svg></button>
)}
{/* 不再允许跳过故事板:统一「确认故事板」。没生成故事板时点它 → 弹气泡提示先去生成,不放行 */}
<div className="sb-confirm-wrap">
{sbConfirmHint && (
<div className="sb-confirm-pop" role="tooltip">
<span className="pop-h">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M12 9v4M12 17h.01" /><path d="M10.3 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" /></svg>
</span>
<span className="pop-body"> <b></b>,</span>
</div>
)}
<button className="btn btn-primary btn-lg" type="button" disabled={loading} onClick={() => { if (!adoptedStoryboard) { setSbConfirmHint(true); return; } goStage(4); }}>, <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M5 12h14M12 5l7 7-7 7" /></svg></button>
</div>
</div>
</div>
</section>