fix: isolate storyboard videos by row
This commit is contained in:
@@ -34,7 +34,6 @@ import {
|
||||
analyzeJob,
|
||||
analyzeProductViews,
|
||||
apiAssetUrl,
|
||||
batchGenerateAll,
|
||||
characterLibraryImageUrl,
|
||||
createAssetLibraryItem,
|
||||
createPromptLibraryItem,
|
||||
@@ -1422,6 +1421,12 @@ function savedScenePatch(scene?: StoryboardScene | null): RowPlanPatch {
|
||||
}
|
||||
}
|
||||
|
||||
function storyboardSceneBelongsToRow(scene: StoryboardScene | null | undefined, rowIndex: number, legacyRowIndex?: number | null) {
|
||||
if (!scene) return false
|
||||
if (typeof scene.storyboard_row_idx === "number") return scene.storyboard_row_idx === rowIndex
|
||||
return legacyRowIndex != null && legacyRowIndex === rowIndex
|
||||
}
|
||||
|
||||
function applyPlanPatch(row: AudioStoryboardRow, patch?: RowPlanPatch): AudioStoryboardRow {
|
||||
if (!patch) return row
|
||||
return {
|
||||
@@ -1726,6 +1731,7 @@ function buildStoryboardSceneFromAudioRow(
|
||||
visual_mode: row.visualMode,
|
||||
needs_product: row.needsProduct,
|
||||
needs_subject: row.needsSubject,
|
||||
storyboard_row_idx: row.index,
|
||||
subject_brief: row.needsSubject ? subjectBrief : "",
|
||||
skg_copy_en: row.skgCopy,
|
||||
skg_copy_zh: row.skgCopyZh,
|
||||
@@ -1733,7 +1739,7 @@ function buildStoryboardSceneFromAudioRow(
|
||||
scene_one_line_zh: row.sceneOneLineZh,
|
||||
action_one_line_en: row.actionOneLine,
|
||||
action_one_line_zh: row.actionOneLineZh,
|
||||
selected_video_id: frame.storyboard?.selected_video_id ?? "",
|
||||
selected_video_id: frame.storyboard?.storyboard_row_idx === row.index ? frame.storyboard?.selected_video_id ?? "" : "",
|
||||
first_frame_plan: row.firstFramePlan,
|
||||
last_frame_plan: row.lastFramePlan,
|
||||
product_placement: row.productPlacement,
|
||||
@@ -3544,8 +3550,23 @@ function AudioStoryboardPlanPanel({
|
||||
})
|
||||
}
|
||||
|
||||
const planForRow = (row: AudioStoryboardRow, frame: KeyFrame | null) =>
|
||||
applyPlanPatch(applyPlanPatch(row, savedScenePatch(frame?.storyboard)), planOverrides[row.index])
|
||||
const referenceFrameForRow = (row: AudioStoryboardRow) =>
|
||||
closestFrameForTime(rowReferencePool, clampNumber((row.start + row.end) / 2, 0, Math.max(job?.duration || row.end, row.end)))
|
||||
|
||||
const legacyRowIndexForFrame = (frameIndex: number) => {
|
||||
for (const item of rows) {
|
||||
if (referenceFrameForRow(item)?.index === frameIndex) return item.index
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const planForRow = (row: AudioStoryboardRow, frame: KeyFrame | null) => {
|
||||
const legacyRowIndex = frame ? legacyRowIndexForFrame(frame.index) : null
|
||||
const savedPatch = storyboardSceneBelongsToRow(frame?.storyboard, row.index, legacyRowIndex)
|
||||
? savedScenePatch(frame?.storyboard)
|
||||
: {}
|
||||
return applyPlanPatch(applyPlanPatch(row, savedPatch), planOverrides[row.index])
|
||||
}
|
||||
|
||||
const rewriteSegmentForRow = (row: AudioStoryboardRow): StoryboardScriptRewriteSegment => ({
|
||||
index: row.index,
|
||||
@@ -3556,12 +3577,22 @@ function AudioStoryboardPlanPanel({
|
||||
current_text: copyForRow(row),
|
||||
})
|
||||
|
||||
const referenceFrameForRow = (row: AudioStoryboardRow) =>
|
||||
closestFrameForTime(rowReferencePool, clampNumber((row.start + row.end) / 2, 0, Math.max(job?.duration || row.end, row.end)))
|
||||
|
||||
const videosForFrame = (frame: KeyFrame | null) => {
|
||||
const videosForRow = (row: AudioStoryboardRow, frame: KeyFrame | null) => {
|
||||
if (!frame) return []
|
||||
return (job?.generated_videos ?? []).filter((video) => video.frame_idx === frame.index)
|
||||
const legacyRowIndex = legacyRowIndexForFrame(frame.index)
|
||||
return (job?.generated_videos ?? []).filter((video) => {
|
||||
if (video.frame_idx !== frame.index) return false
|
||||
if (typeof video.storyboard_row_idx === "number") return video.storyboard_row_idx === row.index
|
||||
return legacyRowIndex === row.index
|
||||
})
|
||||
}
|
||||
|
||||
const selectedVideoIdForRow = (row: AudioStoryboardRow, frame: KeyFrame | null) => {
|
||||
if (!frame?.storyboard) return ""
|
||||
const legacyRowIndex = legacyRowIndexForFrame(frame.index)
|
||||
return storyboardSceneBelongsToRow(frame.storyboard, row.index, legacyRowIndex)
|
||||
? frame.storyboard.selected_video_id ?? ""
|
||||
: ""
|
||||
}
|
||||
|
||||
const quickInputForRow = (row: AudioStoryboardRow, frame: KeyFrame | null): QuickStoryboardPlanInput => ({
|
||||
@@ -3585,12 +3616,17 @@ function AudioStoryboardPlanPanel({
|
||||
selectedVideoId?: string,
|
||||
): StoryboardScene => {
|
||||
const selectedSubjectRefs = row.needsSubject ? selectSubjectRefsForRow(row, subjectRefs) : []
|
||||
const legacyRowIndex = legacyRowIndexForFrame(frame.index)
|
||||
const savedSceneForRow = storyboardSceneBelongsToRow(frame.storyboard, row.index, legacyRowIndex)
|
||||
? frame.storyboard
|
||||
: null
|
||||
const base = buildStoryboardSceneFromAudioRow(row, frame, productItems, selectedSubjectRefs, {
|
||||
firstImage: frame.storyboard?.first_image ?? endpointAssetRef(frame, "first_frame"),
|
||||
lastImage: frame.storyboard?.last_image ?? endpointAssetRef(frame, "last_frame"),
|
||||
firstImage: savedSceneForRow?.first_image ?? endpointAssetRef(frame, "first_frame"),
|
||||
lastImage: savedSceneForRow?.last_image ?? endpointAssetRef(frame, "last_frame"),
|
||||
})
|
||||
const savedSelectedVideoId = selectedVideoIdForRow(row, frame)
|
||||
if (!quickPlan) {
|
||||
return { ...base, selected_video_id: selectedVideoId ?? frame.storyboard?.selected_video_id ?? base.selected_video_id ?? "" }
|
||||
return { ...base, selected_video_id: selectedVideoId ?? savedSelectedVideoId ?? base.selected_video_id ?? "" }
|
||||
}
|
||||
return {
|
||||
...base,
|
||||
@@ -3612,7 +3648,7 @@ function AudioStoryboardPlanPanel({
|
||||
scene: quickPlan.scene || base.scene,
|
||||
product: quickPlan.product || base.product,
|
||||
action: quickPlan.action || base.action,
|
||||
selected_video_id: selectedVideoId ?? frame.storyboard?.selected_video_id ?? base.selected_video_id ?? "",
|
||||
selected_video_id: selectedVideoId ?? savedSelectedVideoId ?? base.selected_video_id ?? "",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3628,10 +3664,10 @@ function AudioStoryboardPlanPanel({
|
||||
"Keep motion natural, creator-ad style, premium wellness lighting, no subtitles, no platform UI, no watermark, no medical treatment claims.",
|
||||
].filter((line) => line.trim()).join("\n")
|
||||
|
||||
const drawVideosForRow = async (row: AudioStoryboardRow, frame: KeyFrame | null, count = 4) => {
|
||||
const drawVideosForRow = async (row: AudioStoryboardRow, frame: KeyFrame | null, count = 4, quiet = false) => {
|
||||
if (!job || !frame) {
|
||||
toast.warning("这条分镜还没有参考帧,先完成抽帧。")
|
||||
return
|
||||
if (!quiet) toast.warning("这条分镜还没有参考帧,先完成抽帧。")
|
||||
return false
|
||||
}
|
||||
const plannedRow = { ...planForRow(row, frame), skgCopy: copyForRow(row), skgCopyZh: copyZhForRow(row) }
|
||||
setQuickVideoBusyRow(row.index)
|
||||
@@ -3644,6 +3680,7 @@ function AudioStoryboardPlanPanel({
|
||||
prompt: promptForStoryboardScene(scene),
|
||||
duration: scene.duration || 4,
|
||||
count,
|
||||
storyboard_row_idx: row.index,
|
||||
first_image: scene.first_image ?? null,
|
||||
last_image: scene.last_image ?? null,
|
||||
product_images: scene.product_images ?? [],
|
||||
@@ -3656,9 +3693,11 @@ function AudioStoryboardPlanPanel({
|
||||
size: "720x1280",
|
||||
})
|
||||
onJobUpdate?.(updated)
|
||||
toast.success(`分镜 ${row.index + 1} 已提交 ${count} 条视频候选`)
|
||||
if (!quiet) toast.success(`分镜 ${row.index + 1} 已提交 ${count} 条视频候选`)
|
||||
return true
|
||||
} catch (e) {
|
||||
toast.error("视频候选生成失败:" + (e instanceof Error ? e.message : String(e)))
|
||||
if (!quiet) toast.error("视频候选生成失败:" + (e instanceof Error ? e.message : String(e)))
|
||||
return false
|
||||
} finally {
|
||||
setQuickVideoBusyRow(null)
|
||||
}
|
||||
@@ -3668,7 +3707,11 @@ function AudioStoryboardPlanPanel({
|
||||
if (!job || !frame) return
|
||||
const plannedRow = { ...planForRow(row, frame), skgCopy: copyForRow(row), skgCopyZh: copyZhForRow(row) }
|
||||
try {
|
||||
const scene = buildSceneForPlannedRow(plannedRow, frame, frame.storyboard, videoId)
|
||||
const legacyRowIndex = legacyRowIndexForFrame(frame.index)
|
||||
const savedSceneForRow = storyboardSceneBelongsToRow(frame.storyboard, row.index, legacyRowIndex)
|
||||
? frame.storyboard
|
||||
: null
|
||||
const scene = buildSceneForPlannedRow(plannedRow, frame, savedSceneForRow, videoId)
|
||||
const updated = await updateStoryboard(job.id, frame.index, scene)
|
||||
onJobUpdate?.(updated)
|
||||
toast.success(`分镜 ${row.index + 1} 已选用该视频`)
|
||||
@@ -4026,16 +4069,17 @@ function AudioStoryboardPlanPanel({
|
||||
if (!job || !rows.length) return
|
||||
const count = clampVideoCount(batchVideoCount)
|
||||
setBatchCardBusy(true)
|
||||
let submitted = 0
|
||||
let failed = 0
|
||||
try {
|
||||
await saveAllStoryboardDrafts(true)
|
||||
const updated = await batchGenerateAll(job.id, {
|
||||
count_per_row: count,
|
||||
concurrency: 1,
|
||||
model: "seedance",
|
||||
size: "720x1280",
|
||||
})
|
||||
onJobUpdate?.(updated)
|
||||
toast.success(`整片视频候选生成已启动:${rows.length} 条分镜 × 每条 ${count} 个候选`)
|
||||
for (const row of rows) {
|
||||
const frame = referenceFrameForRow(row)
|
||||
const ok = await drawVideosForRow(row, frame, count, true)
|
||||
if (ok) submitted += 1
|
||||
else failed += 1
|
||||
}
|
||||
if (failed) toast.warning(`整片已排队 ${submitted} 条分镜,${failed} 条失败或缺少参考帧`)
|
||||
else toast.success(`整片视频候选已按行排队:${submitted} 条分镜 × 每条 ${count} 个候选`)
|
||||
} catch (e) {
|
||||
toast.error("整片视频候选生成失败:" + (e instanceof Error ? e.message : String(e)))
|
||||
} finally {
|
||||
@@ -4395,7 +4439,7 @@ function AudioStoryboardPlanPanel({
|
||||
{rows.map((row) => {
|
||||
const referenceFrame = referenceFrameForRow(row)
|
||||
const plannedRow = planForRow(row, referenceFrame)
|
||||
const rowVideos = videosForFrame(referenceFrame)
|
||||
const rowVideos = videosForRow(row, referenceFrame)
|
||||
const savingStoryboard = storyboardSaveBusyRow === row.index
|
||||
const copyText = copyForRow(row)
|
||||
const copyZhText = copyZhForRow(row)
|
||||
@@ -4543,7 +4587,7 @@ function AudioStoryboardPlanPanel({
|
||||
job={job}
|
||||
videos={rowVideos}
|
||||
enabled={!!referenceFrame}
|
||||
selectedVideoId={referenceFrame?.storyboard?.selected_video_id ?? ""}
|
||||
selectedVideoId={selectedVideoIdForRow(row, referenceFrame)}
|
||||
busy={quickVideoBusyRow === row.index}
|
||||
count={rowVideoCount}
|
||||
onCountChange={(count) => patchRowVideoCount(row.index, count)}
|
||||
@@ -4772,7 +4816,7 @@ function AudioStoryboardPlanPanel({
|
||||
videos={rowVideos}
|
||||
enabled={!!referenceFrame}
|
||||
expanded={videosOpen}
|
||||
selectedVideoId={referenceFrame?.storyboard?.selected_video_id ?? ""}
|
||||
selectedVideoId={selectedVideoIdForRow(row, referenceFrame)}
|
||||
busy={quickVideoBusyRow === row.index}
|
||||
count={rowVideoCount}
|
||||
onCountChange={(count) => patchRowVideoCount(row.index, count)}
|
||||
|
||||
@@ -187,6 +187,7 @@ export interface StoryboardScene {
|
||||
visual_mode?: "person_only" | "person_product" | "product_only" | "environment"
|
||||
needs_product?: boolean
|
||||
needs_subject?: boolean
|
||||
storyboard_row_idx?: number | null
|
||||
subject_brief?: string
|
||||
skg_copy_en?: string
|
||||
skg_copy_zh?: string
|
||||
@@ -241,6 +242,7 @@ export interface GeneratedVideo {
|
||||
id: string
|
||||
provider_id?: string
|
||||
frame_idx: number
|
||||
storyboard_row_idx?: number | null
|
||||
prompt: string
|
||||
model: string
|
||||
status: "queued" | "in_progress" | "completed" | "failed"
|
||||
@@ -1278,6 +1280,7 @@ export async function generateStoryboardVideo(
|
||||
duration?: number
|
||||
count?: number
|
||||
seed?: number | null
|
||||
storyboard_row_idx?: number | null
|
||||
first_image?: ImageRef | null
|
||||
last_image?: ImageRef | null
|
||||
product_images?: ImageRef[]
|
||||
|
||||
Reference in New Issue
Block a user