diff --git a/core/backend/apps/projects/serializers.py b/core/backend/apps/projects/serializers.py index de1d7ee..69879ba 100644 --- a/core/backend/apps/projects/serializers.py +++ b/core/backend/apps/projects/serializers.py @@ -102,15 +102,24 @@ class BaseAssetGroupSerializer(serializers.ModelSerializer): class StoryboardFrameSerializer(serializers.ModelSerializer): asset_url = serializers.SerializerMethodField() + # 分镜图含人脸,后端自动送火山人像审核;暴露审核态让前端挂盾牌(与角色/场景卡同款) + review_status = serializers.SerializerMethodField() + review_error = serializers.SerializerMethodField() class Meta: model = StoryboardFrame - fields = ["id", "script_segment", "asset", "asset_url", "sort_order", "prompt"] + fields = ["id", "script_segment", "asset", "asset_url", "sort_order", "prompt", "review_status", "review_error"] read_only_fields = fields def get_asset_url(self, obj) -> str: return _asset_preview_url(obj.asset) + def get_review_status(self, obj) -> str: + return obj.asset.review_status if obj.asset_id else "" + + def get_review_error(self, obj) -> str: + return (obj.asset.review_error or "") if obj.asset_id else "" + class StoryboardVersionSerializer(serializers.ModelSerializer): frames = StoryboardFrameSerializer(many=True, read_only=True) diff --git a/core/frontend/src/components/review-badge.tsx b/core/frontend/src/components/review-badge.tsx index fccac9c..0ce8863 100644 --- a/core/frontend/src/components/review-badge.tsx +++ b/core/frontend/src/components/review-badge.tsx @@ -11,18 +11,20 @@ const META: Record = { "": { label: "待审核", cls: "rv-idle" } }; -export function ReviewBadge({ status, onSubmit, error, busy }: { +export function ReviewBadge({ status, onSubmit, error, busy, compact }: { status: ReviewStatus; - onSubmit?: () => void; // 仅灰盾(待审核)时点击触发提交 + onSubmit?: () => void; // 灰盾(待审核)/红盾(未过审)时点击触发(重新)送审 error?: string; // 未过审原因(气泡里显) busy?: boolean; // 提交中 + compact?: boolean; // 用在 overflow:hidden 容器里(如故事板分镜图):改用原生 title 提示,避免 CSS 气泡被裁掉看不见 }) { const key = status || ""; const meta = META[key] || META[""]; - const clickable = key === "" && !!onSubmit && !busy; + // 灰盾(待审核)和红盾(未过审/常见是审核超时)都可点重新送审 —— 超时是误判,重提即可再审一遍 + const clickable = (key === "" || key === "failed") && !!onSubmit && !busy; const hint = key === "active" ? "火山人像合规审核 · 已通过,可安全用于视频生成" - : key === "failed" ? `火山人像合规审核 · 未通过${error ? " · " + error : ",建议改提示词重新生成"}` + : key === "failed" ? `火山人像合规审核 · 未通过${error ? " · " + error : ""}${clickable ? " · 点击重新送审" : ",建议改提示词重新生成"}` : key === "processing" ? "火山人像合规审核 · 审核中,通常很快" : clickable ? "尚未送审 · 点击立即提交火山人像审核" : "尚未送审 · 含人脸的视频素材需过火山人像审核"; @@ -31,12 +33,13 @@ export function ReviewBadge({ status, onSubmit, error, busy }: { className={`review-badge ${meta.cls}${clickable ? " clickable" : ""}`} role={clickable ? "button" : undefined} tabIndex={clickable ? 0 : undefined} + title={compact ? hint : undefined} onClick={clickable ? (e) => { e.stopPropagation(); onSubmit!(); } : undefined} onKeyDown={clickable ? (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); e.stopPropagation(); onSubmit!(); } } : undefined} > ); } diff --git a/core/frontend/src/pipeline-page.css b/core/frontend/src/pipeline-page.css index c197086..cdf563c 100644 --- a/core/frontend/src/pipeline-page.css +++ b/core/frontend/src/pipeline-page.css @@ -411,6 +411,10 @@ .sb-gen-veil { position: absolute; inset: 0; z-index: 3; display: grid; place-items: center; border-radius: inherit; pointer-events: none; background: color-mix(in srgb, var(--surface) 62%, transparent); } .sb-scene-thumb .sb-gen-veil .spinner { width: 16px; height: 16px; } .sb-main-img .sb-gen-veil .spinner { width: 28px; height: 28px; } + /* 分镜图火山人像审核盾:缩略图角上只留盾(省地方),大图左上挂完整徽章 */ + .sb-scene-thumb .placeholder .sb-frame-rv { position: absolute; top: 4px; left: 4px; z-index: 4; transform: scale(.82); transform-origin: top left; } + .sb-scene-thumb .placeholder .sb-frame-rv .rv-label { display: none; } + .sb-main-img .sb-main-rv { position: absolute; top: 10px; left: 10px; z-index: 4; } .sb-rerun-note { display: flex; align-items: flex-start; gap: 10px; padding: 10px 12px; margin-bottom: 14px; background: rgba(180,83,9,.08); border: 1px solid rgba(180,83,9,.20); border-radius: var(--r-md); color: #7C3A05; line-height: 1.55; } .sb-rerun-note .warn-ic { width: 22px; height: 22px; border-radius: var(--r-sm); background: rgba(180,83,9,.12); color: #B45309; display: grid; place-items: center; flex: 0 0 22px; } diff --git a/core/frontend/src/routes/pipeline.tsx b/core/frontend/src/routes/pipeline.tsx index 61a4b08..7b99833 100644 --- a/core/frontend/src/routes/pipeline.tsx +++ b/core/frontend/src/routes/pipeline.tsx @@ -947,9 +947,9 @@ export function PipelinePage(props: { setReviewBusyId(null); } } - // 在基础资产趴(stage 2)定时轮询审核状态,刷新徽章(processing → active绿 / failed红) + // 在基础资产趴(stage 2)/ 故事板趴(stage 3)定时轮询审核状态,刷新徽章(processing → active绿 / failed红) useEffect(() => { - if (viewStage !== 2) return; + if (viewStage !== 2 && viewStage !== 3) return; let alive = true; let sawProcessing = false; let timer = 0; @@ -2977,7 +2977,11 @@ export function PipelinePage(props: { const url = frameUrl(frame); return (
setSbSelected(idx)}> -
场 {idx + 1}{sbGenerating &&
+
+ 场 {idx + 1} + {sbGenerating &&
场 {idx + 1}
{times[idx] || `#${frame.sort_order + 1}`}
@@ -3000,6 +3004,7 @@ export function PipelinePage(props: {
setPreview({ src: url, kind: "image", name: `场 ${sbSelected + 1}` }) : undefined} onKeyDown={url ? (event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); setPreview({ src: url, kind: "image", name: `场 ${sbSelected + 1}` }); } } : undefined}> {sbActiveFrame ? `场 ${sbSelected + 1}` : sbExpectedShots ? `场 ${sbSelected + 1} · 待生成` : "// 故事板未生成"} {sbGenerating &&
); })()} diff --git a/core/frontend/src/types.ts b/core/frontend/src/types.ts index 3d712c9..2cf1c53 100644 --- a/core/frontend/src/types.ts +++ b/core/frontend/src/types.ts @@ -331,7 +331,7 @@ export type StoryboardVersion = { id: string; prompt: string; is_adopted: boolean; - frames: Array<{ id: string; sort_order: number; prompt: string; asset: string; asset_url?: string }>; + frames: Array<{ id: string; sort_order: number; prompt: string; asset: string; asset_url?: string; review_status?: string; review_error?: string }>; created_at: string; updated_at: string; };