void
}
-export function StoryboardBar({ job, selectedFrames, focusedFrame, onFocusFrame }: Props) {
+export function StoryboardBar({ job, selectedFrames, focusedFrame, onFocusFrame, onJobUpdate }: Props) {
const [collapsed, setCollapsed] = useState(false)
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
@@ -40,21 +41,19 @@ export function StoryboardBar({ job, selectedFrames, focusedFrame, onFocusFrame
? frames.findIndex((f) => f.index === focusFrame.index) + 1
: 0
- // 所有"已进入分镜阶段"的提取图(按分镜时间序展平)
- type Shot = { frameIdx: number; seq: number; elementId: string; elementName: string; cid: string; isLegacy: boolean }
- const allShots: Shot[] = []
- frames.forEach((f, i) => {
- const seq = i + 1
- ;(f.elements ?? []).forEach((e) => {
- if (e.cutouts && e.cutouts.length > 0) {
- e.cutouts.forEach((cid) => allShots.push({
- frameIdx: f.index, seq, elementId: e.id, elementName: e.name_zh, cid, isLegacy: false,
- }))
- } else if (e.cutout_id) {
- allShots.push({ frameIdx: f.index, seq, elementId: e.id, elementName: e.name_zh, cid: e.cutout_id, isLegacy: true })
- }
- })
- })
+ // 用户已"上推"到分镜头编排区的图片
+ const pushedImages = job.storyboard_images ?? []
+ const frameSeqByIdx: Record
= {}
+ frames.forEach((f, i) => { frameSeqByIdx[f.index] = i + 1 })
+
+ const handleRemovePushed = async (refId: string) => {
+ try {
+ const updated = await removeStoryboardImage(job.id, refId)
+ onJobUpdate?.(updated)
+ } catch (e) {
+ toast.error("移除失败:" + (e instanceof Error ? e.message : String(e)))
+ }
+ }
return (
diff --git a/web/lib/api.ts b/web/lib/api.ts
index d3ecb21..5946258 100644
--- a/web/lib/api.ts
+++ b/web/lib/api.ts
@@ -76,6 +76,16 @@ export interface TranscriptSegment {
zh: string
}
+export interface StoryboardImage {
+ ref_id: string
+ kind: "keyframe" | "cutout"
+ frame_idx: number
+ element_id?: string | null
+ cutout_id?: string | null
+ label?: string
+ created_at?: number
+}
+
export interface Job {
id: string
url: string
@@ -88,6 +98,7 @@ export interface Job {
height?: number
frames: KeyFrame[]
transcript: TranscriptSegment[]
+ storyboard_images?: StoryboardImage[]
error?: string
}
@@ -248,6 +259,31 @@ export function representativeCutoutUrl(
return null
}
+export async function pushStoryboardImage(
+ jobId: string,
+ body: { kind: "keyframe" | "cutout"; frame_idx: number; element_id?: string | null; cutout_id?: string | null; label?: string },
+): Promise {
+ const res = await fetch(`${API_BASE}/jobs/${jobId}/storyboard-images`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(body),
+ })
+ if (!res.ok) {
+ const txt = await res.text().catch(() => "")
+ throw new Error(`pushStoryboardImage ${res.status} ${txt.slice(0, 300)}`)
+ }
+ return res.json()
+}
+
+export async function removeStoryboardImage(jobId: string, refId: string): Promise {
+ const res = await fetch(`${API_BASE}/jobs/${jobId}/storyboard-images/${refId}`, { method: "DELETE" })
+ if (!res.ok) {
+ const txt = await res.text().catch(() => "")
+ throw new Error(`removeStoryboardImage ${res.status} ${txt.slice(0, 300)}`)
+ }
+ return res.json()
+}
+
export async function updateStoryboard(
jobId: string,
frameIdx: number,