auto-save 2026-05-14 05:16 (~3)
This commit is contained in:
@@ -3309,6 +3309,19 @@
|
||||
"type": "session-heartbeat",
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 3 项未提交变更 · 最近提交:auto-save 2026-05-14 05:05 (~6)",
|
||||
"files_changed": 3
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-14T05:10:53+08:00",
|
||||
"type": "commit",
|
||||
"message": "auto-save 2026-05-14 05:10 (~3)",
|
||||
"hash": "c7ca830",
|
||||
"files_changed": 3
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-13T21:13:13Z",
|
||||
"type": "session-heartbeat",
|
||||
"message": "Claude 会话活跃 · 最近命令:claude · 2 项未提交变更 · 最近提交:auto-save 2026-05-14 05:10 (~3)",
|
||||
"files_changed": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -47,6 +47,15 @@ const LIVING_VIEW_OPTIONS = [
|
||||
["action_use", "使用"],
|
||||
]
|
||||
|
||||
type LightboxTab = "clean" | "scene" | "subject" | "review"
|
||||
|
||||
const LIGHTBOX_TABS: Array<{ key: LightboxTab; label: string }> = [
|
||||
{ key: "clean", label: "原图/清洗" },
|
||||
{ key: "scene", label: "场景图" },
|
||||
{ key: "subject", label: "主体包" },
|
||||
{ key: "review", label: "审核" },
|
||||
]
|
||||
|
||||
export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, onChange, onToggleSelect, onJobUpdate, onSwitchPanel, onCopyImage, embedded = false }: Props) {
|
||||
const [describing, setDescribing] = useState(false)
|
||||
const [cleaning, setCleaning] = useState(false)
|
||||
@@ -60,6 +69,7 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
|
||||
const [subjectKinds, setSubjectKinds] = useState<Record<string, SubjectKind>>({})
|
||||
const [subjectBackgrounds, setSubjectBackgrounds] = useState<Record<string, AssetBackground>>({})
|
||||
const [subjectViews, setSubjectViews] = useState<Record<string, string[]>>({})
|
||||
const [activeTab, setActiveTab] = useState<LightboxTab>("clean")
|
||||
const [editingElement, setEditingElement] = useState<{
|
||||
id: string
|
||||
name_zh: string
|
||||
@@ -119,6 +129,12 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
|
||||
const latestSceneAsset = f.scene_assets?.[f.scene_assets.length - 1] ?? null
|
||||
const selectedFrameIndices = Array.from(selected).sort((a, b) => a - b)
|
||||
const sharedSubjectFrameIndices = selectedFrameIndices.length > 1 ? selectedFrameIndices : [f.index]
|
||||
const subjectAssetCount = elements.reduce((sum, item) => sum + (item.subject_assets?.length ?? 0), 0)
|
||||
const cutoutCount = elements.reduce((sum, item) => sum + ((item.cutouts?.length ?? 0) || (item.cutout_id ? 1 : 0)), 0)
|
||||
const qualityWarnings = [
|
||||
...(f.quality_report?.warnings ?? []),
|
||||
...(latestSceneAsset?.quality_report?.warnings ?? []),
|
||||
]
|
||||
|
||||
const handleDescribe = async () => {
|
||||
setDescribing(true)
|
||||
@@ -424,6 +440,28 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-1 border-b border-white/10 bg-black/28 px-3 py-2">
|
||||
{LIGHTBOX_TABS.map((tab) => (
|
||||
<button
|
||||
key={tab.key}
|
||||
type="button"
|
||||
onClick={() => setActiveTab(tab.key)}
|
||||
className={`h-7 rounded-md px-2.5 text-[11px] font-medium transition ${
|
||||
activeTab === tab.key
|
||||
? "bg-white text-black shadow"
|
||||
: "bg-white/[0.06] text-white/58 hover:bg-white/[0.12] hover:text-white"
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
<div className="ml-auto hidden items-center gap-2 text-[10px] text-white/42 sm:flex">
|
||||
<span>{latestSceneAsset ? "场景已生成" : "场景待生成"}</span>
|
||||
<span>·</span>
|
||||
<span>{subjectAssetCount > 0 ? `${subjectAssetCount} 主体资产` : "主体待生成"}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 主体 — 左:大图 + 清洗 / 选用;右:识别 + 元素清单 */}
|
||||
<div className="flex gap-3 p-3 overflow-hidden flex-1 min-h-0">
|
||||
{/* 左侧大图区 */}
|
||||
@@ -485,6 +523,8 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
|
||||
)}
|
||||
</div>
|
||||
|
||||
{activeTab === "clean" && (
|
||||
<>
|
||||
{/* 画框工具栏 */}
|
||||
{cropMode ? (
|
||||
extractNamePrompt ? (
|
||||
@@ -623,7 +663,10 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
|
||||
{cleaning ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Sparkle className="h-3.5 w-3.5" />}
|
||||
{cleaning ? "清洗中…(5-15 秒)" : hasCleaned ? "重新清洗" : f.cleaned_applied ? "再次清洗" : "🧹 清洗水印"}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeTab === "scene" && (
|
||||
<div className="rounded-lg border border-white/10 bg-white/[0.035] p-2">
|
||||
<div className="mb-2 flex items-center justify-between gap-2">
|
||||
<div className="text-[11.5px] font-semibold text-white">场景图</div>
|
||||
@@ -673,6 +716,37 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
|
||||
{sceneGenerating ? "生成场景图中…" : latestSceneAsset ? "重新生成场景图" : "生成场景图"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === "review" && (
|
||||
<div className="space-y-2 rounded-lg border border-white/10 bg-white/[0.035] p-2">
|
||||
<div className="text-[11.5px] font-semibold text-white">素材审核</div>
|
||||
<div className="grid grid-cols-2 gap-1.5 text-[10.5px]">
|
||||
<div className={`rounded border px-2 py-1 ${f.cleaned_applied || hasCleaned ? "border-emerald-300/30 bg-emerald-500/10 text-emerald-100" : "border-white/10 bg-black/25 text-white/55"}`}>
|
||||
{f.cleaned_applied ? "已应用清洗" : hasCleaned ? "清洗待确认" : "未清洗"}
|
||||
</div>
|
||||
<div className={`rounded border px-2 py-1 ${latestSceneAsset ? "border-emerald-300/30 bg-emerald-500/10 text-emerald-100" : "border-white/10 bg-black/25 text-white/55"}`}>
|
||||
{latestSceneAsset ? "场景图已生成" : "场景图未生成"}
|
||||
</div>
|
||||
<div className={`rounded border px-2 py-1 ${subjectAssetCount > 0 ? "border-violet-300/35 bg-violet-500/12 text-violet-100" : "border-white/10 bg-black/25 text-white/55"}`}>
|
||||
{subjectAssetCount > 0 ? `${subjectAssetCount} 张主体资产` : "主体包未生成"}
|
||||
</div>
|
||||
<div className={`rounded border px-2 py-1 ${qualityWarnings.length ? "border-amber-300/35 bg-amber-500/12 text-amber-100" : "border-emerald-300/30 bg-emerald-500/10 text-emerald-100"}`}>
|
||||
{qualityWarnings.length ? `${qualityWarnings.length} 个风险` : "质量可用"}
|
||||
</div>
|
||||
</div>
|
||||
{qualityWarnings.length > 0 && (
|
||||
<div className="rounded border border-amber-300/25 bg-amber-500/10 px-2 py-1.5 text-[10px] leading-snug text-amber-100/85">
|
||||
{qualityWarnings.slice(0, 3).map((warning, i) => (
|
||||
<div key={i}>{warning}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-[10px] leading-relaxed text-white/42">
|
||||
审核通过后,把场景图和主体资产复制到分镜槽位;当前不会自动覆盖素材,避免模型误改细节。
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => onToggleSelect(f.index)}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
Link2, Upload, Download, Scissors, Image as ImageIcon,
|
||||
Mic, Languages, FileEdit, Film, FileVideo, Loader2, Plus, X, LayoutGrid, Maximize2,
|
||||
Copy, Trash2, Move, PanelLeft, PanelRight, PanelBottom, ChevronLeft, ChevronRight, SlidersHorizontal,
|
||||
CheckCircle2, AlertTriangle, Sparkles, Package,
|
||||
} from "lucide-react"
|
||||
import { toast } from "sonner"
|
||||
import { NodeShell, type NodeStatus, type NodeKind } from "./node-shell"
|
||||
@@ -1129,6 +1130,8 @@ const THUMB_W = 64
|
||||
const THUMB_GAP = 6
|
||||
|
||||
type ElementPreview = { frameIdx: number; elementId: string; name: string; src: string; cid: string; timestamp: number }
|
||||
type SceneAssetPreview = { frameIdx: number; assetId: string; label: string; src: string; width: number; height: number; risk?: string }
|
||||
type SubjectAssetPreview = { frameIdx: number; elementId: string; assetId: string; label: string; src: string; width: number; height: number; view: string }
|
||||
|
||||
function collectElementCrops(job: Job | null): ElementPreview[] {
|
||||
return job
|
||||
@@ -1154,6 +1157,41 @@ function collectElementCrops(job: Job | null): ElementPreview[] {
|
||||
: []
|
||||
}
|
||||
|
||||
function collectSceneAssets(job: Job | null): SceneAssetPreview[] {
|
||||
return job
|
||||
? job.frames.flatMap((f) =>
|
||||
(f.scene_assets ?? []).map((asset) => ({
|
||||
frameIdx: f.index,
|
||||
assetId: asset.id,
|
||||
label: asset.label || `分镜 ${f.index + 1} 场景图`,
|
||||
src: apiAssetUrl(asset.url),
|
||||
width: asset.width,
|
||||
height: asset.height,
|
||||
risk: asset.quality_report?.risk,
|
||||
})).filter((p) => p.src),
|
||||
)
|
||||
: []
|
||||
}
|
||||
|
||||
function collectSubjectAssets(job: Job | null): SubjectAssetPreview[] {
|
||||
return job
|
||||
? job.frames.flatMap((f) =>
|
||||
(f.elements ?? []).flatMap((element) =>
|
||||
(element.subject_assets ?? []).map((asset) => ({
|
||||
frameIdx: f.index,
|
||||
elementId: element.id,
|
||||
assetId: asset.id,
|
||||
label: asset.label || `${element.name_zh} · ${asset.view}`,
|
||||
src: apiAssetUrl(asset.url),
|
||||
width: asset.width,
|
||||
height: asset.height,
|
||||
view: asset.view,
|
||||
})),
|
||||
).filter((p) => p.src),
|
||||
)
|
||||
: []
|
||||
}
|
||||
|
||||
function videoModelLabel(model: string) {
|
||||
const m = model.toLowerCase()
|
||||
if (m.includes("kling")) return "Kling"
|
||||
@@ -1175,10 +1213,18 @@ export function VisualLabNode({ data, selected }: any) {
|
||||
? `${job.width}/${job.height}`
|
||||
: "9/16"
|
||||
const elementCrops = collectElementCrops(job)
|
||||
const sceneAssets = collectSceneAssets(job)
|
||||
const subjectAssets = collectSubjectAssets(job)
|
||||
const cleanedCount = frames.filter((x) => x.cleaned_url).length
|
||||
const cutoutCount = frames.reduce((s, x) => s + (x.elements?.filter((e) => hasCutout(e)).length ?? 0), 0)
|
||||
const sceneAssetCount = frames.reduce((s, x) => s + (x.scene_assets?.length ?? 0), 0)
|
||||
const subjectAssetCount = frames.reduce((s, x) => s + (x.elements?.reduce((n, e) => n + (e.subject_assets?.length ?? 0), 0) ?? 0), 0)
|
||||
const sceneAssetCount = sceneAssets.length
|
||||
const subjectAssetCount = subjectAssets.length
|
||||
const selectedFrameCount = frames.filter((f) => d.selectedFrames.has(f.index)).length
|
||||
const targetFrameCount = selectedFrameCount || frames.length
|
||||
const qualityRiskCount = frames.filter((f) => f.quality_report?.risk && f.quality_report.risk !== "ok").length
|
||||
const preparedUnits = Math.min(targetFrameCount, sceneAssetCount) + (subjectAssetCount > 0 ? 1 : 0)
|
||||
const totalUnits = Math.max(1, targetFrameCount + 1)
|
||||
const prepPct = Math.min(100, Math.round((preparedUnits / totalUnits) * 100))
|
||||
const runningVideo = videos.some((v) => v.status === "queued" || v.status === "in_progress")
|
||||
const completedVideos = videos.filter((v) => v.status === "completed" && v.url)
|
||||
const failedVideo = videos.some((v) => v.status === "failed")
|
||||
@@ -1191,9 +1237,11 @@ export function VisualLabNode({ data, selected }: any) {
|
||||
: keyframeStatus(job)
|
||||
|
||||
type VisualPreview =
|
||||
| { id: string; kind: "frame"; frameIdx: number; src: string; label: string; caption: string; borderClass: string }
|
||||
| { id: string; kind: "cutout"; frameIdx: number; elementId: string; cutoutId: string; src: string; label: string; caption: string; borderClass: string }
|
||||
| { id: string; kind: "video"; videoId: string; videoSrc?: string; posterSrc?: string; label: string; caption: string; borderClass: string }
|
||||
| { id: string; kind: "frame"; group: string; frameIdx: number; src: string; label: string; caption: string; borderClass: string; aspect: string }
|
||||
| { id: string; kind: "scene"; group: string; frameIdx: number; assetId: string; src: string; label: string; caption: string; borderClass: string; aspect: string }
|
||||
| { id: string; kind: "subject"; group: string; frameIdx: number; assetId: string; src: string; label: string; caption: string; borderClass: string; aspect: string }
|
||||
| { id: string; kind: "cutout"; group: string; frameIdx: number; elementId: string; cutoutId: string; src: string; label: string; caption: string; borderClass: string; aspect: string }
|
||||
| { id: string; kind: "video"; group: string; videoId: string; videoSrc?: string; posterSrc?: string; label: string; caption: string; borderClass: string; aspect: string }
|
||||
|
||||
const [hoverPreview, setHoverPreview] = useState<PreviewAnchor<string> | null>(null)
|
||||
const [pinnedPreview, setPinnedPreview] = useState<PreviewAnchor<string> | null>(null)
|
||||
@@ -1204,15 +1252,42 @@ export function VisualLabNode({ data, selected }: any) {
|
||||
...(job && jobId ? frames.map((f) => ({
|
||||
id: `frame:${f.index}`,
|
||||
kind: "frame" as const,
|
||||
group: "关键帧",
|
||||
frameIdx: f.index,
|
||||
src: effectiveFrameUrl(jobId, f),
|
||||
label: `分镜 ${f.index + 1}`,
|
||||
caption: `${f.timestamp.toFixed(2)}s`,
|
||||
borderClass: "border-orange-300/50",
|
||||
caption: `${f.timestamp.toFixed(2)}s${f.quality_report?.risk && f.quality_report.risk !== "ok" ? " · 风险" : ""}`,
|
||||
borderClass: f.quality_report?.risk === "bad" ? "border-rose-300/70" : f.quality_report?.risk === "warn" ? "border-amber-300/70" : "border-orange-300/50",
|
||||
aspect,
|
||||
})) : []),
|
||||
...sceneAssets.map((p) => ({
|
||||
id: `scene:${p.frameIdx}:${p.assetId}`,
|
||||
kind: "scene" as const,
|
||||
group: "场景图",
|
||||
frameIdx: p.frameIdx,
|
||||
assetId: p.assetId,
|
||||
src: p.src,
|
||||
label: p.label,
|
||||
caption: `${p.width}×${p.height}`,
|
||||
borderClass: p.risk === "bad" ? "border-rose-300/70" : p.risk === "warn" ? "border-amber-300/70" : "border-emerald-300/60",
|
||||
aspect: p.width && p.height ? `${p.width}/${p.height}` : aspect,
|
||||
})),
|
||||
...subjectAssets.map((p) => ({
|
||||
id: `subject:${p.frameIdx}:${p.assetId}`,
|
||||
kind: "subject" as const,
|
||||
group: "主体包",
|
||||
frameIdx: p.frameIdx,
|
||||
assetId: p.assetId,
|
||||
src: p.src,
|
||||
label: p.label,
|
||||
caption: `${p.width}×${p.height}`,
|
||||
borderClass: "border-violet-300/65",
|
||||
aspect: p.width && p.height ? `${p.width}/${p.height}` : "1/1",
|
||||
})),
|
||||
...elementCrops.map((p) => ({
|
||||
id: `cutout:${p.frameIdx}:${p.elementId}:${p.cid}`,
|
||||
kind: "cutout" as const,
|
||||
group: "普通抠图",
|
||||
frameIdx: p.frameIdx,
|
||||
elementId: p.elementId,
|
||||
cutoutId: p.cid,
|
||||
@@ -1220,6 +1295,7 @@ export function VisualLabNode({ data, selected }: any) {
|
||||
label: p.name,
|
||||
caption: `分镜 ${p.frameIdx + 1}`,
|
||||
borderClass: "border-violet-300/60",
|
||||
aspect: "1/1",
|
||||
})),
|
||||
...videos.map((v, i) => {
|
||||
const videoSrc = apiAssetUrl(v.url)
|
||||
@@ -1227,12 +1303,14 @@ export function VisualLabNode({ data, selected }: any) {
|
||||
return {
|
||||
id: `video:${v.id}`,
|
||||
kind: "video" as const,
|
||||
group: "视频任务",
|
||||
videoId: v.id,
|
||||
videoSrc: v.status === "completed" && videoSrc ? videoSrc : undefined,
|
||||
posterSrc: posterSrc || undefined,
|
||||
label: `视频 ${i + 1}`,
|
||||
caption: `${videoModelLabel(v.model)} · ${v.status}`,
|
||||
borderClass: v.status === "completed" ? "border-emerald-300/60" : v.status === "failed" ? "border-rose-300/70" : "border-violet-300/55",
|
||||
aspect,
|
||||
}
|
||||
}),
|
||||
]
|
||||
@@ -1266,11 +1344,14 @@ export function VisualLabNode({ data, selected }: any) {
|
||||
p.kind === "frame"
|
||||
? isSelected ? "border-emerald-400 ring-2 ring-emerald-400/60" : "border-white/30 dark:border-white/20"
|
||||
: p.borderClass
|
||||
} ${p.kind === "cutout" ? "bg-white" : "bg-black"}`}
|
||||
style={{ height: THUMBNAIL_HEIGHT, aspectRatio: aspect }}
|
||||
} ${p.kind === "cutout" || p.kind === "subject" ? "bg-white" : "bg-black"}`}
|
||||
style={{ height: THUMBNAIL_HEIGHT, aspectRatio: p.aspect }}
|
||||
onMouseEnter={(e) => setHoverPreview({ id: p.id, ...canvasThumbnailAnchor(rootRef.current, e.currentTarget) })}
|
||||
onMouseLeave={() => setHoverPreview(null)}
|
||||
>
|
||||
<div className="absolute -top-6 left-0 z-[68] rounded bg-black/75 px-1.5 py-0.5 text-[9px] font-medium text-white/80 backdrop-blur">
|
||||
{p.group}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
@@ -1279,6 +1360,16 @@ export function VisualLabNode({ data, selected }: any) {
|
||||
setPinnedPreview((prev) => (prev?.id === p.id ? null : { id: p.id, ...anchor }))
|
||||
if (p.kind === "frame") {
|
||||
;(d.onOpenFramePanel ?? d.onExpandFrame)(p.frameIdx)
|
||||
} else if (p.kind === "scene" || p.kind === "subject") {
|
||||
d.onCopyImage?.({
|
||||
kind: "asset",
|
||||
frame_idx: p.frameIdx,
|
||||
element_id: p.assetId,
|
||||
cutout_id: p.assetId,
|
||||
label: p.label,
|
||||
})
|
||||
if (!d.selectedFrames.has(p.frameIdx)) d.onToggleFrame(p.frameIdx)
|
||||
d.onOpenWorkbench?.(p.frameIdx)
|
||||
} else if (p.kind === "cutout") {
|
||||
if (!d.selectedFrames.has(p.frameIdx)) d.onToggleFrame(p.frameIdx)
|
||||
d.onOpenStoryboard?.(p.frameIdx)
|
||||
@@ -1291,7 +1382,7 @@ export function VisualLabNode({ data, selected }: any) {
|
||||
}
|
||||
}
|
||||
}}
|
||||
title={`${p.label} · 单击钉住预览${p.kind === "frame" ? " / 打开镜头处理" : p.kind === "cutout" ? " / 进入分镜编排" : " / 复制 prompt"}`}
|
||||
title={`${p.label} · 单击钉住预览${p.kind === "frame" ? " / 打开素材审核" : p.kind === "video" ? " / 复制 prompt" : " / 复制到分镜编排"}`}
|
||||
className="absolute inset-0 h-full w-full overflow-hidden rounded-md"
|
||||
>
|
||||
{p.kind === "video" ? (
|
||||
@@ -1303,13 +1394,13 @@ export function VisualLabNode({ data, selected }: any) {
|
||||
<div className="absolute inset-0 bg-violet-950/50" />
|
||||
)
|
||||
) : (
|
||||
<img src={p.src} alt={p.label} className={`absolute inset-0 h-full w-full ${p.kind === "cutout" ? "object-contain" : "object-cover"}`} />
|
||||
<img src={p.src} alt={p.label} className={`absolute inset-0 h-full w-full ${p.kind === "frame" ? "object-cover" : "object-contain"}`} />
|
||||
)}
|
||||
{p.kind === "frame" && isSelected && (
|
||||
<div className="absolute inset-0 bg-emerald-400/15 rounded-md pointer-events-none" />
|
||||
)}
|
||||
<div className="absolute bottom-0 right-0 bg-black/70 px-1 py-0.5 text-[8.5px] font-mono leading-none text-white rounded-bl rounded-br-md">
|
||||
{p.kind === "frame" ? p.caption.replace("s", "") + "s" : p.kind === "cutout" ? "元素" : "视频"}
|
||||
{p.kind === "frame" ? p.caption.replace("s", "") + "s" : p.kind === "scene" ? "场景" : p.kind === "subject" ? "主体" : p.kind === "cutout" ? "抠图" : "视频"}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -1327,6 +1418,26 @@ export function VisualLabNode({ data, selected }: any) {
|
||||
</button>
|
||||
)}
|
||||
|
||||
{(p.kind === "scene" || p.kind === "subject") && d.onCopyImage && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
d.onCopyImage?.({
|
||||
kind: "asset",
|
||||
frame_idx: p.frameIdx,
|
||||
element_id: p.assetId,
|
||||
cutout_id: p.assetId,
|
||||
label: p.label,
|
||||
})
|
||||
}}
|
||||
title="复制此素材(到分镜头编排工作台插槽粘贴)"
|
||||
className="absolute top-1.5 left-1.5 z-[70] inline-flex h-7 w-7 items-center justify-center rounded-full bg-violet-500/95 text-white shadow-lg backdrop-blur transition hover:scale-110 hover:bg-violet-400"
|
||||
>
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{p.kind === "cutout" && d.onCopyImage && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -1435,7 +1546,7 @@ export function VisualLabNode({ data, selected }: any) {
|
||||
imgSrc={p.kind === "video" ? p.posterSrc : p.src}
|
||||
videoSrc={p.kind === "video" ? p.videoSrc : undefined}
|
||||
poster={p.kind === "video" ? p.posterSrc : undefined}
|
||||
aspect={aspect}
|
||||
aspect={p.aspect}
|
||||
label={p.label}
|
||||
caption={p.caption}
|
||||
borderClass={p.borderClass}
|
||||
@@ -1458,39 +1569,94 @@ export function VisualLabNode({ data, selected }: any) {
|
||||
pinned={d.pinnedNodes?.has("visual")}
|
||||
onTogglePin={() => d.onToggleNodePin?.("visual")}
|
||||
>
|
||||
<div className="grid grid-cols-3 gap-2 text-[10.5px] text-[var(--text-soft)]">
|
||||
<div className="rounded-md border border-white/10 bg-black/20 p-2">
|
||||
<div className="mb-1.5 flex items-center justify-between gap-2">
|
||||
<div className="text-[11px] font-semibold text-[var(--text-strong)]">素材准备进度</div>
|
||||
<div className="font-mono text-[10px] text-[var(--text-faint)]">{prepPct}%</div>
|
||||
</div>
|
||||
<div className="h-2 overflow-hidden rounded-full bg-white/10">
|
||||
<div
|
||||
className="h-full rounded-full bg-emerald-400 transition-all"
|
||||
style={{ width: `${prepPct}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-1.5 flex items-center justify-between gap-2 text-[9.5px] text-[var(--text-faint)]">
|
||||
<span>{targetFrameCount} 张目标帧</span>
|
||||
{qualityRiskCount > 0 ? (
|
||||
<span className="inline-flex items-center gap-1 text-amber-300/85">
|
||||
<AlertTriangle className="h-2.5 w-2.5" />
|
||||
{qualityRiskCount} 个质量风险
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1 text-emerald-300/80">
|
||||
<CheckCircle2 className="h-2.5 w-2.5" />
|
||||
无质量风险
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 grid grid-cols-2 gap-2 text-[10.5px] text-[var(--text-soft)]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => { e.stopPropagation(); openFirstFrame() }}
|
||||
disabled={frames.length === 0}
|
||||
className="rounded-md border border-white/10 px-2 py-1.5 text-left transition hover:border-orange-300/50 hover:bg-orange-400/10 disabled:opacity-35"
|
||||
title="打开镜头处理面板"
|
||||
className="min-h-14 rounded-md border border-white/10 px-2 py-2 text-left transition hover:border-orange-300/50 hover:bg-orange-400/10 disabled:opacity-35"
|
||||
title="打开素材准备 / 审核面板"
|
||||
>
|
||||
<div className="text-[var(--text-strong)] text-[12px] font-semibold">{frames.length}</div>
|
||||
<div>关键帧</div>
|
||||
<div className="mb-1 flex items-center gap-1 text-[var(--text-strong)] text-[12px] font-semibold">
|
||||
<ImageIcon className="h-3 w-3 text-orange-300" />
|
||||
{targetFrameCount}/{frames.length}
|
||||
</div>
|
||||
<div>目标关键帧</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => { e.stopPropagation(); openFirstFrame() }}
|
||||
disabled={!job || frames.length === 0}
|
||||
className="min-h-14 rounded-md border border-white/10 px-2 py-2 text-left transition hover:border-emerald-300/50 hover:bg-emerald-400/10 disabled:opacity-35"
|
||||
title="生成 / 审核每张关键帧的场景图"
|
||||
>
|
||||
<div className="mb-1 flex items-center gap-1 text-[var(--text-strong)] text-[12px] font-semibold">
|
||||
<Sparkles className="h-3 w-3 text-emerald-300" />
|
||||
{sceneAssetCount}/{targetFrameCount}
|
||||
</div>
|
||||
<div>场景图</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => { e.stopPropagation(); openFirstFrame() }}
|
||||
disabled={!job || frames.length === 0}
|
||||
className="min-h-14 rounded-md border border-white/10 px-2 py-2 text-left transition hover:border-violet-300/50 hover:bg-violet-400/10 disabled:opacity-35"
|
||||
title="生成主体多视角 / 动作 / 表情资产包"
|
||||
>
|
||||
<div className="mb-1 flex items-center gap-1 text-[var(--text-strong)] text-[12px] font-semibold">
|
||||
<Package className="h-3 w-3 text-violet-300" />
|
||||
{subjectAssetCount || cutoutCount}
|
||||
</div>
|
||||
<div>主体资产</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => { e.stopPropagation(); d.onOpenWorkbench?.(frames.find((f) => d.selectedFrames.has(f.index))?.index ?? frames[0]?.index) }}
|
||||
disabled={!job || frames.length === 0}
|
||||
className="rounded-md border border-white/10 px-2 py-1.5 text-left transition hover:border-violet-300/50 hover:bg-violet-400/10 disabled:opacity-35"
|
||||
title="进入分镜编排"
|
||||
className="min-h-14 rounded-md border border-white/10 px-2 py-2 text-left transition hover:border-pink-300/50 hover:bg-pink-400/10 disabled:opacity-35"
|
||||
title="进入分镜编排和视频生成"
|
||||
>
|
||||
<div className="text-[var(--text-strong)] text-[12px] font-semibold">{elementCrops.length}</div>
|
||||
<div>元素 / 编排</div>
|
||||
<div className="mb-1 flex items-center gap-1 text-[var(--text-strong)] text-[12px] font-semibold">
|
||||
<Film className="h-3 w-3 text-pink-300" />
|
||||
{videos.length}
|
||||
</div>
|
||||
<div>分镜 / 视频</div>
|
||||
</button>
|
||||
<div className="rounded-md border border-white/10 px-2 py-1.5">
|
||||
<div className="text-[var(--text-strong)] text-[12px] font-semibold">{videos.length}</div>
|
||||
<div>视频任务</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 text-[10.5px] leading-snug text-[var(--text-faint)]">
|
||||
{frames.length > 0 ? (
|
||||
<>
|
||||
{cleanedCount} 已清洗 · {sceneAssetCount} 场景图 · {subjectAssetCount || cutoutCount} 主体素材 · {d.selectedFrames.size}/{frames.length} 入编排 · {completedVideos.length} 已完成
|
||||
{cleanedCount} 已清洗 · {sceneAssetCount} 场景图 · {subjectAssetCount || cutoutCount} 主体素材 · {selectedFrameCount}/{frames.length} 入编排 · {completedVideos.length} 已完成
|
||||
</>
|
||||
) : (
|
||||
"解析后这里展示关键帧、元素和视频任务;具体处理仍在点击后的工作台完成。"
|
||||
"解析后这里变成素材准备看板:先审关键帧,再生成场景图和主体资产包。"
|
||||
)}
|
||||
</div>
|
||||
</NodeShell>
|
||||
|
||||
Reference in New Issue
Block a user