feat(storyboard): 故事板改分镜制(单场独立重跑/历史)+ 视频过审闸 + 审核前端如实化
三块改动在 services/views/pipeline/App/api 等文件按行交织、且过审闸后端依赖故事板新 模型,无法拆成各自独立可编译的提交,故合并为一次: · 故事板分镜制:新增 StoryboardShot/StoryboardShotVersion(对标 VideoSegment/Version) + 数据迁移(采用版每帧转一 shot+已采用版本)。每镜一个 shot,各自历史版本,可单场 重跑、逐场切换/采用,新增「开始生成故事板/单场重跑/采用某版」端点。重跑非阻塞(后台 静默轮询出图,不锁其它场按钮);历史「查看」与「采用」分离(切版本不丢在制重跑)。 视频取分镜图/删分镜清理/过审闸全部改读 storyboard_shots。 · 视频生成前过审闸:点生成校验含真人脸的人物立绘/故事板分镜是否过审,未过审弹窗列出 哪一镜/哪个,可一键送审,挡住不生成(collect_video_review_blockers + precheck 端点)。 · 审核前端如实化:不再 `|| "processing"` 伪造审核中;送审失败经 onNotify 如实提示; 审核轮询扩到视频阶段,送审后能看着盾变绿。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+30
-13
@@ -357,6 +357,25 @@ export function App() {
|
||||
}
|
||||
}, [activeProjectId]);
|
||||
|
||||
// 静默轮询故事板分镜生成(对标 pollVideosQuiet):有 queued/running 的场就驱动后端出图并刷新,不占全局 loading、不弹 toast。
|
||||
// 这样单场重跑不会把别的场按钮也锁住——生成在后台跑,UI 只按每场自身状态转圈。
|
||||
const pollStoryboardQuiet = useCallback(async () => {
|
||||
if (!activeProjectId) return;
|
||||
let detail = projectDetailRef.current;
|
||||
if (!detail || detail.id !== activeProjectId) {
|
||||
detail = await api.project(activeProjectId).catch(() => null);
|
||||
if (!detail) return;
|
||||
setProjectDetail(detail);
|
||||
}
|
||||
const active = (detail.storyboard_shots ?? []).filter((s) => ["queued", "running"].includes(s.status));
|
||||
if (active.length === 0) return;
|
||||
await api.pollStoryboard(activeProjectId).catch(() => undefined);
|
||||
const next = await api.project(activeProjectId).catch(() => null);
|
||||
if (next && next.id === activeProjectIdRef.current) {
|
||||
setProjectDetail((prev) => (prev && JSON.stringify(prev) === JSON.stringify(next) ? prev : next));
|
||||
}
|
||||
}, [activeProjectId]);
|
||||
|
||||
// 静默刷新导出任务状态(进入拼接页 / 导出后回填成片),不弹 toast。
|
||||
const refreshExport = useCallback(async () => {
|
||||
if (!activeProjectId) return;
|
||||
@@ -866,7 +885,9 @@ export function App() {
|
||||
notice={notice}
|
||||
unreadCount={unreadCount}
|
||||
avatarChar={avatarChar}
|
||||
logout={logout} onAdoptScript={(scriptId) => action(() => api.adoptScript(pipelineProject.id, scriptId), "脚本已采用")}
|
||||
logout={logout}
|
||||
onNotify={(type, text) => setNotice({ type, text })}
|
||||
onAdoptScript={(scriptId) => action(() => api.adoptScript(pipelineProject.id, scriptId), "脚本已采用")}
|
||||
onUpdateShot={(payload) => action(() => api.updateScriptSegment(pipelineProject.id, payload), "分镜已更新", { liteRefresh: true })}
|
||||
onAddShot={(afterSegmentId, content) => action(() => api.addScriptSegment(pipelineProject.id, { after_segment_id: afterSegmentId, ...content }), "分镜已添加", { liteRefresh: true })}
|
||||
onDeleteShot={(segmentId) => action(() => api.deleteScriptSegment(pipelineProject.id, { segment_id: segmentId }), "分镜已删除", { liteRefresh: true })}
|
||||
@@ -883,19 +904,15 @@ export function App() {
|
||||
return assetId ? { adopted_asset: assetId } : null;
|
||||
}}
|
||||
onGenerateStoryboard={(prompt) =>
|
||||
action(async () => {
|
||||
// 异步故事板:提交(秒回)后轮询;后端在后台线程逐帧生成,poll 永远秒回,故每轮间隔等待
|
||||
await api.generateStoryboard(pipelineProject.id, { prompt });
|
||||
for (let i = 0; i < 60; i += 1) {
|
||||
const res = await api.pollStoryboard(pipelineProject.id);
|
||||
if (res.status === "succeeded") return true;
|
||||
if (res.status === "failed") throw new Error(res.error || "故事板生成失败,请重试");
|
||||
await new Promise((resolve) => setTimeout(resolve, 4000));
|
||||
}
|
||||
// 轮询窗口耗尽仍未完成:如实报超时,不能让 action 弹「已生成」的假成功 toast
|
||||
throw new Error("故事板生成超时,请稍后刷新查看或重试");
|
||||
}, "故事板已生成")
|
||||
// 只「提交」(秒回);出图由后台 pollStoryboardQuiet 驱动 —— 不占全局 loading,不锁其它场按钮(对标视频「开始生成」)
|
||||
action(() => api.generateStoryboard(pipelineProject.id, { prompt }), "故事板已开始生成")
|
||||
}
|
||||
onRerunStoryboardShot={(shotId, prompt) =>
|
||||
// 单场重跑也只「提交」(秒回);后台轮询出图。不阻塞 → 可同时重跑别的场
|
||||
action(() => api.rerunStoryboardShot(pipelineProject.id, shotId, prompt), "本场已开始重跑")
|
||||
}
|
||||
onAdoptStoryboardShotVersion={(shotId, versionId) => action(() => api.adoptStoryboardShotVersion(pipelineProject.id, shotId, versionId), "已采用该版本")}
|
||||
onPollStoryboardQuiet={pollStoryboardQuiet}
|
||||
onAdoptBaseAsset={(groupId, assetId) => action(() => api.adoptBaseAsset(pipelineProject.id, { group_id: groupId, asset_id: assetId }), "已采用该候选")}
|
||||
onSetAdoptState={(groupId, state) => action(() => api.setBaseAssetAdopt(pipelineProject.id, { group_id: groupId, state }), state === "adopted" ? "已采用" : "已标为未采用")}
|
||||
onDeleteBaseAsset={(groupId) => action(() => api.deleteBaseAsset(pipelineProject.id, groupId), "已删除")}
|
||||
|
||||
Reference in New Issue
Block a user