auto-save 2026-05-13 17:50 (~4)
This commit is contained in:
@@ -4,10 +4,10 @@ import { createPortal } from "react-dom"
|
||||
import { type NodeProps } from "@xyflow/react"
|
||||
import {
|
||||
Link2, Upload, Download, Scissors, Image as ImageIcon,
|
||||
Mic, Languages, FileEdit, Sparkles, Film, FileVideo, Loader2, Plus, X, LayoutGrid,
|
||||
Mic, Languages, FileEdit, Film, FileVideo, Loader2, Plus, X, LayoutGrid,
|
||||
} from "lucide-react"
|
||||
import { NodeShell, type NodeStatus, type NodeKind } from "./node-shell"
|
||||
import { type Job, type ImageRef, frameUrl, effectiveFrameUrl, videoUrl, generatedImageUrl, cutoutUrl, hasCutout, representativeCutoutUrl } from "@/lib/api"
|
||||
import { type Job, type ImageRef, effectiveFrameUrl, videoUrl, hasCutout, representativeCutoutUrl } from "@/lib/api"
|
||||
|
||||
export interface NodeData {
|
||||
job: Job | null // 当前 active job
|
||||
@@ -31,7 +31,7 @@ export interface NodeData {
|
||||
onDeleteFrame?: (idx: number) => void // 删整张关键帧
|
||||
onDeleteGenerated?: (frameIdx: number, genId: string) => void // 删单张生成图
|
||||
onOpenStoryboard?: (frameIdx: number) => void // 打开分镜头编排专属面板
|
||||
onPushToStoryboard?: (payload: { kind: "keyframe" | "cutout"; frameIdx: number; elementId?: string; cutoutId?: string; label?: string }) => void
|
||||
onOpenWorkbench?: () => void // 打开全屏分镜编排工作台
|
||||
onCopyImage?: (ref: ImageRef) => void // 复制图片到全局剪贴板(粘贴到分镜头编排工作台插槽)
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ export function InputNode({ data, selected }: NodeProps<{ data: NodeData }> | an
|
||||
// 是否已下载 → 显示视频 + 解析按钮
|
||||
const hasVideo = !!job?.video_url
|
||||
const isDownloading = job?.status === "downloading" || job?.status === "created"
|
||||
const isAnalyzing = !!job && ["splitting", "frames_extracted", "transcribing"].includes(job.status)
|
||||
const isAnalyzing = !!job && ["splitting", "transcribing"].includes(job.status)
|
||||
const isDone = job?.status === "transcribed"
|
||||
const hasFrames = (job?.frames.length ?? 0) > 0
|
||||
const inputLocked = isDownloading || d.submitting
|
||||
@@ -466,9 +466,9 @@ export function KeyframeNode({ data, selected }: any) {
|
||||
|
||||
<NodeShell
|
||||
type="process" status={st}
|
||||
icon={<ImageIcon className="h-4 w-4" />}
|
||||
title="关键帧 · 清洗 + 提取"
|
||||
subtitle={`STEP 2 · ${frames.length ? `${d.selectedFrames.size}/${frames.length} 选用` : "等待抽取"}`}
|
||||
icon={<ImageIcon className="h-4 w-4" />}
|
||||
title="镜头拆解 · 元素提取"
|
||||
subtitle={`STEP 2 · ${frames.length ? `${d.selectedFrames.size}/${frames.length} 入编排` : "等待抽取"}`}
|
||||
width={KEYFRAME_WIDTH}
|
||||
selected={selected}
|
||||
>
|
||||
@@ -485,7 +485,7 @@ export function KeyframeNode({ data, selected }: any) {
|
||||
<span className={cutoutCount > 0 ? "text-violet-300/90 font-medium" : ""}>{cutoutCount}/{elementsCount} 已抠图</span>
|
||||
<br />
|
||||
<span className="text-[10.5px] text-[var(--text-faint)]">
|
||||
点缩略图 → 清洗水印 / 提取元素 → 抠图给「分镜头编排」用
|
||||
点缩略图 → 清洗水印 / 提取可借鉴元素 → 改造成 SKG 画面素材
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
@@ -509,8 +509,8 @@ export function ASRNode({ data, selected }: any) {
|
||||
<NodeShell
|
||||
type="ai" status={asrStatus(d.job)}
|
||||
icon={<Mic className="h-4 w-4" />}
|
||||
title="转录 · ASR"
|
||||
subtitle="STEP 5 · Gemini"
|
||||
title="声音文案 · ASR"
|
||||
subtitle="STEP 3 · 可选文案轨"
|
||||
selected={selected}
|
||||
>
|
||||
<div className="text-[11.5px] text-[var(--text-soft)]">
|
||||
@@ -550,8 +550,8 @@ export function TranslateNode({ data, selected }: any) {
|
||||
<NodeShell
|
||||
type="ai" status={st}
|
||||
icon={<Languages className="h-4 w-4" />}
|
||||
title="翻译 · Translate"
|
||||
subtitle="STEP 6 · EN → ZH"
|
||||
title="翻译理解 · Translate"
|
||||
subtitle="STEP 4 · EN → ZH"
|
||||
selected={selected}
|
||||
>
|
||||
<div className="text-[11.5px] text-[var(--text-soft)]">
|
||||
@@ -576,12 +576,12 @@ export function RewriteNode({ selected }: any) {
|
||||
<NodeShell
|
||||
type="ai" status="pending"
|
||||
icon={<FileEdit className="h-4 w-4" />}
|
||||
title="文案改写 · Rewrite"
|
||||
subtitle="STEP 7 · 接产品信息"
|
||||
title="产品文案 · Rewrite"
|
||||
subtitle="STEP 5 · 接 SKG 卖点"
|
||||
selected={selected}
|
||||
>
|
||||
<textarea
|
||||
placeholder="粘贴 SKG 产品信息 / 关键卖点(占位,未接通)"
|
||||
placeholder="粘贴 SKG 产品信息 / 关键卖点(可作为视频脚本和镜头动作参考)"
|
||||
rows={3}
|
||||
disabled
|
||||
className="w-full text-[11.5px] px-2.5 py-2 rounded-md bg-white/30 dark:bg-white/[0.03] border border-dashed border-black/15 dark:border-white/10 placeholder:text-[var(--text-faint)] text-[var(--text-strong)] resize-none opacity-70"
|
||||
@@ -592,11 +592,11 @@ export function RewriteNode({ selected }: any) {
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
8. ImageGenNode — 显示 selected frames 的代表生成图
|
||||
6. StoryboardNode — 元素改造 + 分镜编排入口
|
||||
============================================================ */
|
||||
const IMAGEGEN_WIDTH = 360
|
||||
|
||||
export function ImageGenNode({ data, selected }: any) {
|
||||
export function StoryboardNode({ data, selected }: any) {
|
||||
const d: NodeData = data
|
||||
const job = d?.job
|
||||
|
||||
@@ -618,7 +618,8 @@ export function ImageGenNode({ data, selected }: any) {
|
||||
: []
|
||||
|
||||
const totalElements = elementCrops.length
|
||||
const status: NodeStatus = !job ? "pending" : totalElements > 0 ? "done" : "pending"
|
||||
const storyboardCount = job?.frames.filter((f) => d.selectedFrames.has(f.index)).length ?? 0
|
||||
const status: NodeStatus = !job ? "pending" : storyboardCount > 0 || totalElements > 0 ? "done" : "pending"
|
||||
const aspect = job && job.height > 0 ? `${job.width}/${job.height}` : "9/16"
|
||||
|
||||
return (
|
||||
@@ -638,8 +639,12 @@ export function ImageGenNode({ data, selected }: any) {
|
||||
style={{ aspectRatio: aspect }}
|
||||
>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); d.onOpenStoryboard?.(p.frameIdx) }}
|
||||
title={`${p.name} · 来自分镜 ${p.frameIdx + 1} · hover 看大图`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
d.onOpenStoryboard?.(p.frameIdx)
|
||||
d.onOpenWorkbench?.()
|
||||
}}
|
||||
title={`${p.name} · 来自分镜 ${p.frameIdx + 1} · 点击进入分镜编排`}
|
||||
className="absolute inset-0 w-full h-full"
|
||||
>
|
||||
<img
|
||||
@@ -696,26 +701,27 @@ export function ImageGenNode({ data, selected }: any) {
|
||||
<NodeShell
|
||||
type="ai" status={status}
|
||||
icon={<LayoutGrid className="h-4 w-4" />}
|
||||
title="分镜头编排 · Storyboard"
|
||||
subtitle={`STEP 6 · 接元素 + 场景${totalElements > 0 ? ` · ${totalElements} 个元素` : ""}`}
|
||||
title="元素改造 · Storyboard"
|
||||
subtitle={`STEP 6 · 参考元素 → SKG 画面${storyboardCount > 0 ? ` · ${storyboardCount} 分镜` : ""}`}
|
||||
width={IMAGEGEN_WIDTH}
|
||||
selected={selected}
|
||||
>
|
||||
{totalElements > 0 ? (
|
||||
<div className="text-[11.5px] leading-relaxed text-[var(--text-soft)]">
|
||||
素材:<span className="text-[var(--text-strong)] font-medium">{totalElements}</span> 个元素 + 干净版场景
|
||||
<br />
|
||||
<span className="text-[10.5px] text-[var(--text-faint)]">
|
||||
上方缩略图点击进入编排 · 多视角 / 风格融合 / 布局在此完成(Phase 2)
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-[11.5px] text-[var(--text-faint)] leading-relaxed">
|
||||
<span className="text-[var(--text-strong)]">编排素材待接入</span>
|
||||
<br />
|
||||
<span className="text-[10.5px]">到关键帧节点画框 → 裁切元素 → 这里聚合所有素材做分镜头编排</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="text-[11.5px] leading-relaxed text-[var(--text-soft)]">
|
||||
不是复刻原视频:先把参考图里的主体 / 场景 / 动作 / 道具拆出来,再替换成 SKG 产品画面。
|
||||
<br />
|
||||
<span className="text-[10.5px] text-[var(--text-faint)]">
|
||||
已有 {totalElements} 个提取元素 · {storyboardCount} 个分镜进入编排
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => { e.stopPropagation(); d.onOpenWorkbench?.() }}
|
||||
disabled={!job || storyboardCount === 0}
|
||||
className="mt-2 w-full rounded-md bg-gradient-to-r from-violet-500 to-pink-500 px-3 py-2 text-[12px] font-semibold text-white shadow-lg shadow-violet-500/25 transition hover:opacity-95 disabled:cursor-not-allowed disabled:opacity-35"
|
||||
title={storyboardCount === 0 ? "先在关键帧节点选用分镜" : "进入 4 图槽分镜编排"}
|
||||
>
|
||||
进入分镜编排
|
||||
</button>
|
||||
</NodeShell>
|
||||
|
||||
</div>
|
||||
@@ -730,8 +736,8 @@ export function VideoGenNode({ selected }: any) {
|
||||
<NodeShell
|
||||
type="ai" status="pending"
|
||||
icon={<Film className="h-4 w-4" />}
|
||||
title="生视频 · Video Gen"
|
||||
subtitle="STEP 9 · 多家可切"
|
||||
title="生成视频 · Video Gen"
|
||||
subtitle="STEP 7 · 首帧 + 动作 prompt"
|
||||
selected={selected}
|
||||
>
|
||||
<div className="grid grid-cols-3 gap-1.5 text-[10.5px]">
|
||||
@@ -754,7 +760,7 @@ export function ComposeNode({ selected }: any) {
|
||||
type="output" status="pending"
|
||||
icon={<FileVideo className="h-4 w-4" />}
|
||||
title="合成成品 · Compose"
|
||||
subtitle="STEP 10 · ffmpeg + TTS"
|
||||
subtitle="STEP 8 · ffmpeg + 字幕"
|
||||
selected={selected}
|
||||
hasSource={false}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user