auto-save 2026-05-14 12:37 (~6)
This commit is contained in:
@@ -1,19 +1,5 @@
|
||||
{
|
||||
"entries": [
|
||||
{
|
||||
"files_changed": 1,
|
||||
"hash": "714db7d",
|
||||
"message": "auto-save 2026-05-13 05:46 (~1)",
|
||||
"ts": "2026-05-13T05:46:19+08:00",
|
||||
"type": "commit"
|
||||
},
|
||||
{
|
||||
"files_changed": 1,
|
||||
"hash": "91d2d64",
|
||||
"message": "auto-save 2026-05-13 05:52 (~1)",
|
||||
"ts": "2026-05-13T05:52:14+08:00",
|
||||
"type": "commit"
|
||||
},
|
||||
{
|
||||
"files_changed": 1,
|
||||
"hash": "4a9264a",
|
||||
@@ -3289,6 +3275,19 @@
|
||||
"type": "session-heartbeat",
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 2 项未提交变更 · 最近提交:auto-save 2026-05-14 12:26 (~4)",
|
||||
"files_changed": 2
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-14T12:32:00+08:00",
|
||||
"type": "commit",
|
||||
"message": "auto-save 2026-05-14 12:31 (~2)",
|
||||
"hash": "01ab67e",
|
||||
"files_changed": 2
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-14T04:36:11Z",
|
||||
"type": "session-heartbeat",
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 6 项未提交变更 · 最近提交:auto-save 2026-05-14 12:31 (~2)",
|
||||
"files_changed": 6
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -629,7 +629,7 @@ api/main.py
|
||||
</div>
|
||||
<div class="flow-row">
|
||||
<div><strong>你看到的区域</strong><span>关键帧素材审核面板</span></div>
|
||||
<div><strong>主要源码</strong><span><code>FrameLightbox</code>;按“原图/清洗、主体资产、首尾帧、产品融合、审核”五个页签组织;左侧只放主图/框选画布,但主体资产页左侧改为全部已清洗/已选参考帧网格,首尾帧页左侧显示全部关键帧并可勾选人物/机位参考。主体识别页会显示透明骨架人目标和 Vision 验收分数。清洗页右侧支持一键清洗未处理帧、单张替换清洗版和一键替换全部待应用清洗版;批量替换顺序调用 <code>applyCleanedFrame</code>,不新增后端接口。产品融合页左侧是纵向 6 行镜头工作表:每行直接显示首帧、尾帧、同一产品 4 个角度图、描述词、秒数和单条生成按钮,便于一次看完 6 条视频。产品融合槽位的“粘贴”优先使用应用内 <code>clipboard</code>,也支持选中槽位后 Cmd+V 粘贴系统图片。右侧保留 GPT Image 2 / Seedance 固定模型、当前镜头状态、桌面四角度一键填充、20 条产品使用描述模板、批量排队和产品图库选用;产品图库选中后会填入当前镜头下一个产品角度槽。主体资产页只确认一个统一主体,后端按参考重绘六张纯背景、占满画面的标准站立透明骨架人资产图;首尾帧页通过地点、风格、参考要素和可编辑 prompt 做文字生图,生成结果写入 <code>scene_assets</code> 但以 <code>asset_role=first_frame/last_frame</code> 标记,并自动传入当前产品融合镜头。相关接口包括 <code>cleanupFrame</code>、<code>applyCleanedFrame</code>、<code>addElement</code>、<code>generateSubjectAssets</code>、<code>generateSceneAsset</code>、<code>listProductLibrary</code>、<code>copyProductLibraryAsset</code> 和 <code>generateProductFusionDescriptions</code>。</span></div>
|
||||
<div><strong>主要源码</strong><span><code>FrameLightbox</code>;按“原图/清洗、主体资产、首尾帧、产品融合、审核”五个页签组织;左侧只放主图/框选画布,但主体资产页左侧改为全部已清洗/已选参考帧网格,首尾帧页左侧显示全部关键帧并可勾选人物/机位参考。主体识别页会显示透明骨架人目标和 Vision 验收分数。清洗页右侧支持一键清洗未处理帧、单张替换清洗版和一键替换全部待应用清洗版;批量替换顺序调用 <code>applyCleanedFrame</code>,不新增后端接口。产品融合页左侧是纵向 6 行镜头工作表:每行只显示首帧、尾帧、已预填动作描述、秒数、生成按钮和对应视频结果;四张桌面 SKG 产品图作为固定产品参考,生成时通过 <code>copyProductLibraryAsset</code> 自动写入镜头,不再暴露产品角度槽、产品融合辅助栏或产品图库选择器。产品融合槽位的“粘贴”优先使用应用内 <code>clipboard</code>,也支持选中槽位后 Cmd+V 粘贴系统图片。主体资产页只确认一个统一主体,后端按参考重绘六张纯背景、占满画面的标准站立透明骨架人资产图;首尾帧页通过地点、风格、参考要素和可编辑 prompt 做文字生图,生成结果写入 <code>scene_assets</code> 但以 <code>asset_role=first_frame/last_frame</code> 标记,并自动传入当前产品融合镜头。相关接口包括 <code>cleanupFrame</code>、<code>applyCleanedFrame</code>、<code>addElement</code>、<code>generateSubjectAssets</code>、<code>generateSceneAsset</code> 和 <code>copyProductLibraryAsset</code>。</span></div>
|
||||
<div><strong>适合怎么描述</strong><span>“这一组关键帧如何共同生成一个统一主体包;某张关键帧的水印、去主体场景图、产品融合镜头组和质量风险应该如何审核”。</span></div>
|
||||
</div>
|
||||
<div class="flow-row">
|
||||
@@ -748,7 +748,7 @@ SubjectAsset {
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>ProductFusionShot</h3>
|
||||
<p>产品融合镜头组的单行数据。每个关键帧最多 6 行,首帧、尾帧、四张同一产品不同角度图、动作描述和秒数一一对应;生成时直接把首尾帧和产品角度图作为 Seedance 垫图提交。</p>
|
||||
<p>产品融合镜头组的单行数据。每个关键帧最多 6 行,用户只补首帧、尾帧和必要时微调动作描述、秒数;四张桌面 SKG 产品角度图固定隐藏填充,生成时直接把首尾帧和固定产品图作为 Seedance 垫图提交。</p>
|
||||
<pre>ProductFusionShot {
|
||||
id,
|
||||
first_image,
|
||||
@@ -806,7 +806,7 @@ SubjectAsset {
|
||||
<tr><td>产品图库</td><td><code>GET /product-library/skg</code></td><td><code>listProductLibrary</code></td><td>读取内置 SKG 白底图库 manifest,返回产品标题、品类、尺寸、白底评分和预览图 URL。</td></tr>
|
||||
<tr><td>产品图入库到 job</td><td><code>POST /jobs/{id}/assets/product-library</code></td><td><code>copyProductLibraryAsset</code></td><td>把一个内置产品图库条目复制为当前 job 的普通 asset,返回 <code>ImageRef(kind="asset")</code>,用于画面工作台产品融合和分镜产品参考组。</td></tr>
|
||||
<tr><td>产品融合引导图</td><td><code>POST /jobs/{id}/product-fusion/guide</code></td><td><code>createProductFusionGuide</code></td><td>旧流程兼容接口:读取产品图和白底人物图,按 <code>product_region</code> 合成位置引导图。当前首尾帧流程不再主动调用它。</td></tr>
|
||||
<tr><td>产品融合描述词</td><td><code>POST /jobs/{id}/product-fusion/descriptions</code></td><td><code>generateProductFusionDescriptions</code></td><td>生成 20 条产品融合动作描述库,前端每次按 6 条轮换套用到 6 行镜头;输入重点变为首帧、尾帧和四张产品角度图,有 LLM 配置时用 <code>REWRITE_MODEL</code> 生成 JSON,无配置或失败时回退到本地 20 条精准模板。</td></tr>
|
||||
<tr><td>产品融合描述词</td><td><code>POST /jobs/{id}/product-fusion/descriptions</code></td><td><code>generateProductFusionDescriptions</code></td><td>兼容接口:可生成 20 条产品融合动作描述库。当前前端默认直接用本地 20 条精准模板预填 6 行镜头,不再显示单独的 AI 草拟入口。</td></tr>
|
||||
<tr><td>分镜保存</td><td><code>PUT /frames/{idx}/storyboard</code></td><td><code>updateStoryboard</code></td><td>保存 4 图槽、时长和改造说明。</td></tr>
|
||||
<tr><td>生图</td><td><code>POST /frames/{idx}/generate</code></td><td><code>generateImage</code></td><td>基于关键帧或已选生成图做 image-to-image,目前可用。</td></tr>
|
||||
</tbody>
|
||||
@@ -925,7 +925,7 @@ SubjectAsset {
|
||||
</header>
|
||||
<div class="body">
|
||||
<p><strong>问题:</strong>产品融合视频的动作描述不能泛泛写“人物使用产品”,需要稳定表达透明骨架人在具体场景中佩戴 SKG 产品,并呈现舒适享受状态。</p>
|
||||
<p><strong>改动:</strong>前端内置 20 条产品使用描述模板,覆盖卧室、客厅、办公、浴室、阳台、影棚、阅读角等场景;“AI 草拟 6 条”每次从 20 条中按 6 条轮换套用,便于多次生成不同镜头组。</p>
|
||||
<p><strong>改动:</strong>前端内置 20 条产品使用描述模板,覆盖卧室、客厅、办公、浴室、阳台、影棚、阅读角等场景;当前产品融合页默认把前 6 条预填到 6 行镜头,用户只在需要时直接改每行描述。</p>
|
||||
<p><strong>后端:</strong><code>generateProductFusionDescriptions</code> 的兜底模板同步扩为 20 条,LLM 提示也改为生成 20 条 35-70 字描述,要求包含场景、佩戴/展示动作和舒适表情,同时排除医疗治疗承诺。</p>
|
||||
<p><strong>影响:</strong><code>api/main.py</code>、<code>web/components/lightbox.tsx</code>、<code>docs/source-analysis.html</code>。</p>
|
||||
</div>
|
||||
@@ -1089,6 +1089,18 @@ SubjectAsset {
|
||||
<p><strong>影响:</strong><code>web/components/lightbox.tsx</code>、<code>docs/source-analysis.html</code>。</p>
|
||||
</div>
|
||||
</article>
|
||||
<article class="change">
|
||||
<header>
|
||||
<h3>2026-05-14 · 产品融合收敛为首尾帧 + 固定四产品图</h3>
|
||||
<span class="tag violet">FrameLightbox</span>
|
||||
<span class="tag orange">产品融合</span>
|
||||
</header>
|
||||
<div class="body">
|
||||
<p><strong>问题:</strong>产品融合页继续显示产品角度槽、辅助栏和产品图库会把操作变复杂;当前工作流只需要用户手动补人物首尾帧,产品图固定来自桌面 4 张 SKG 图。</p>
|
||||
<p><strong>改动:</strong>“产品融合”页每行只保留首帧、尾帧、已预填描述词、秒数、生成按钮和行末视频结果。生成单条或批量视频前,前端自动把内置的 4 张桌面 SKG 产品图复制为当前 job asset 并写入 <code>product_images[4]</code>;视频 prompt 增加 <code>产品融合镜头ID</code> 标记,用来把生成结果显示回对应行。</p>
|
||||
<p><strong>影响:</strong><code>web/components/lightbox.tsx</code>、<code>web/app/page.tsx</code>、<code>web/components/nodes/index.tsx</code>、<code>web/components/dashboard.tsx</code>、<code>docs/source-analysis.html</code>。</p>
|
||||
</div>
|
||||
</article>
|
||||
<article class="change">
|
||||
<header>
|
||||
<h3>2026-05-14 · 产品融合镜头组改为纵向 6 行工作表</h3>
|
||||
|
||||
@@ -522,13 +522,14 @@ export default function Home() {
|
||||
if (!frame) return
|
||||
const productRefs = (shot.product_images ?? []).filter(Boolean).slice(0, 4) as ImageRef[]
|
||||
if (!shot.first_image || !shot.last_image || productRefs.length < 4 || !shot.action_text?.trim()) {
|
||||
toast.error("产品融合镜头缺少首帧、尾帧、四张产品角度图或描述词")
|
||||
toast.error("产品融合镜头缺少首帧、尾帧、固定产品图或描述词")
|
||||
return
|
||||
}
|
||||
const duration = shot.duration && shot.duration > 0 ? shot.duration : 5
|
||||
const labelOf = (ref?: ImageRef | null, fallback = "未提供") => ref?.label || fallback
|
||||
try {
|
||||
const prompt = [
|
||||
`产品融合镜头ID:${shot.id || `shot-${frameIdx + 1}`}`,
|
||||
`竖屏 9:16,${duration.toFixed(1)} 秒,Seedance 图生视频。`,
|
||||
"图片模型固定为 GPT Image 2:首帧和尾帧已经由文字生图生成,用来锁定透明骨架人角色、场景构图和动作起止状态。",
|
||||
"视频模型固定为 Seedance:使用首帧作为起始画面、尾帧作为结束画面,并用四张同一 SKG 产品不同角度白底图作为垫图/产品身份参考。",
|
||||
|
||||
@@ -320,6 +320,7 @@ export const Dashboard = forwardRef<DashboardHandle, Props>(function Dashboard({
|
||||
embedded
|
||||
jobId={data.job.id}
|
||||
frames={data.job.frames}
|
||||
generatedVideos={data.job.generated_videos ?? []}
|
||||
activeIndex={data.expandedFrame}
|
||||
selected={data.selectedFrames}
|
||||
onClose={data.onCloseExpandedFrame}
|
||||
@@ -333,6 +334,7 @@ export const Dashboard = forwardRef<DashboardHandle, Props>(function Dashboard({
|
||||
clipboard={data.clipboard}
|
||||
onCopyImage={data.onCopyImage}
|
||||
onGenerateProductFusionVideo={data.onGenerateProductFusionVideo}
|
||||
onDeleteVideo={data.onDeleteVideo}
|
||||
/>
|
||||
) : (
|
||||
renderSection(t.key)
|
||||
|
||||
@@ -5,7 +5,7 @@ import { X, ChevronLeft, ChevronRight, Check, Sparkles, Wand2, Loader2, Eye, Ref
|
||||
import {
|
||||
frameUrl, cleanedFrameUrl, apiAssetUrl,
|
||||
describeFrame, cleanupFrame, applyCleanedFrame, discardCleanedFrame, addElement, updateElement, deleteElement,
|
||||
generateSceneAsset, generateSubjectAssets, generateProductFusionDescriptions, resolveImageRefUrl, uploadStoryboardAsset, updateStoryboard, copyProductLibraryAsset,
|
||||
generateSceneAsset, generateSubjectAssets, resolveImageRefUrl, uploadStoryboardAsset, updateStoryboard, copyProductLibraryAsset,
|
||||
type AssetBackground, type AssetSize, type KeyFrame, type Job, type ImageRef, type ProductFusionShot, type SceneAssetRole, type SceneStyle, type SubjectKind,
|
||||
} from "@/lib/api"
|
||||
import { TRANSPARENT_HUMAN_FRAME_STANDARD, TRANSPARENT_HUMAN_UI_SUMMARY } from "@/lib/workflow-target"
|
||||
@@ -200,8 +200,6 @@ export function FrameLightbox({ jobId, frames, generatedVideos = [], activeIndex
|
||||
const [fusionUploadTarget, setFusionUploadTarget] = useState<FusionUploadTarget | null>(null)
|
||||
const [fusionGenerating, setFusionGenerating] = useState<number | "all" | null>(null)
|
||||
const [fusionSaving, setFusionSaving] = useState(false)
|
||||
const [fusionFillingProducts, setFusionFillingProducts] = useState<"current" | "all" | null>(null)
|
||||
const [fusionDraftPage, setFusionDraftPage] = useState(0)
|
||||
const [editingElement, setEditingElement] = useState<{
|
||||
frameIndex: number
|
||||
id: string
|
||||
@@ -338,7 +336,6 @@ export function FrameLightbox({ jobId, frames, generatedVideos = [], activeIndex
|
||||
sceneExtraKeywords.trim() ? `额外关键词:${sceneExtraKeywords.trim()}。` : "",
|
||||
"要求:单一透明骨架人清晰可见,人物占画面主体,首尾帧可连续生成视频;无文字、水印、平台 UI、恐怖解剖感。",
|
||||
].filter(Boolean).join("\n")
|
||||
const currentFusionShot = fusionShots[activeFusionShot] ?? fusionShots[0]
|
||||
const fusionReadyCount = fusionShots.filter((shot) =>
|
||||
shot.first_image && shot.last_image && shot.action_text?.trim()
|
||||
).length
|
||||
@@ -397,31 +394,7 @@ export function FrameLightbox({ jobId, frames, generatedVideos = [], activeIndex
|
||||
requestAnimationFrame(() => fusionFileInputRef.current?.click())
|
||||
}
|
||||
|
||||
const draftFusionDescriptions = async () => {
|
||||
const actions = PRODUCT_FUSION_DESCRIPTION_PRESETS
|
||||
let descriptions = actions
|
||||
try {
|
||||
const result = await generateProductFusionDescriptions(jobId, fusionShots)
|
||||
descriptions = result.descriptions.length >= PRODUCT_FUSION_DESCRIPTION_PRESETS.length ? result.descriptions : actions
|
||||
} catch (e) {
|
||||
toast.error("AI 描述生成失败,已使用本地草稿")
|
||||
}
|
||||
const start = (fusionDraftPage * FUSION_SHOT_COUNT) % descriptions.length
|
||||
const selectedDescriptions = Array.from({ length: FUSION_SHOT_COUNT }, (_, i) => (
|
||||
descriptions[(start + i) % descriptions.length] || actions[i]
|
||||
))
|
||||
const next = fusionShots.map((shot, i) => ({
|
||||
...shot,
|
||||
action_text: selectedDescriptions[i] || shot.action_text || actions[i],
|
||||
}))
|
||||
setFusionShots(next)
|
||||
setFusionDraftPage((prev) => prev + 1)
|
||||
void persistFusionShots(next)
|
||||
toast.success(`已套用 6 条动作描述 · 模板 ${start + 1}-${Math.min(start + FUSION_SHOT_COUNT, descriptions.length)}`)
|
||||
}
|
||||
|
||||
const ensureFixedProductAngles = async (indexes: number[]) => {
|
||||
setFusionFillingProducts("all")
|
||||
try {
|
||||
const reusableRefs = fusionShots.find((shot) => (shot.product_images ?? []).filter(Boolean).length >= PRODUCT_ANGLE_COUNT)
|
||||
?.product_images?.slice(0, PRODUCT_ANGLE_COUNT)
|
||||
@@ -439,8 +412,6 @@ export function FrameLightbox({ jobId, frames, generatedVideos = [], activeIndex
|
||||
} catch (e) {
|
||||
toast.error("桌面产品角度填充失败:" + (e instanceof Error ? e.message : String(e)))
|
||||
return null
|
||||
} finally {
|
||||
setFusionFillingProducts(null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1628,94 +1599,6 @@ export function FrameLightbox({ jobId, frames, generatedVideos = [], activeIndex
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
{activeTab === "product" && (
|
||||
<>
|
||||
<section className="rounded-lg border border-amber-300/18 bg-amber-500/[0.08] p-2.5 text-[10.5px] leading-relaxed text-white/58">
|
||||
<div className="mb-2 flex items-center justify-between gap-2">
|
||||
<div className="text-[12px] font-semibold text-white">产品融合辅助</div>
|
||||
<span className="text-[9px] text-white/40">{fusionSaving ? "保存中" : "自动保存"}</span>
|
||||
</div>
|
||||
<div className="mb-2 grid grid-cols-2 gap-1.5">
|
||||
<div className="rounded border border-white/10 bg-black/25 px-2 py-1">
|
||||
<div className="text-[9px] text-white/35">图片模型</div>
|
||||
<div className="text-white/80">GPT Image 2</div>
|
||||
</div>
|
||||
<div className="rounded border border-white/10 bg-black/25 px-2 py-1">
|
||||
<div className="text-[9px] text-white/35">视频模型</div>
|
||||
<div className="text-white/80">Seedance</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-2 rounded-md border border-white/10 bg-black/25 px-2 py-1.5">
|
||||
<div className="mb-1 flex items-center justify-between gap-2">
|
||||
<span className="font-mono text-[10px] text-amber-100">当前镜头 {activeFusionShot + 1}</span>
|
||||
<span className="text-[9px] text-white/38">{currentFusionShot?.duration ?? 5}s</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-x-2 gap-y-1 text-[9.5px]">
|
||||
<span className={currentFusionFirstUrl ? "text-emerald-200/80" : "text-white/35"}>首帧</span>
|
||||
<span className={currentFusionLastUrl ? "text-emerald-200/80" : "text-white/35"}>尾帧</span>
|
||||
<span className={currentFusionProductCount >= PRODUCT_ANGLE_COUNT ? "text-emerald-200/80" : "text-white/35"}>产品角度 {currentFusionProductCount}/{PRODUCT_ANGLE_COUNT}</span>
|
||||
<span className={currentFusionShot?.action_text?.trim() ? "text-emerald-200/80" : "text-white/35"}>描述词</span>
|
||||
</div>
|
||||
<div className={`mt-1 truncate text-[9.5px] ${currentFusionShot?.action_text?.trim() ? "text-white/58" : "text-white/32"}`}>
|
||||
{currentFusionShot?.action_text?.trim() || "描述词未填写"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-2 grid grid-cols-2 gap-1.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void fillDesktopProductAngles("current")}
|
||||
disabled={!!fusionFillingProducts}
|
||||
className="rounded-md border border-amber-300/20 bg-amber-500/15 px-2 py-1.5 text-[10.5px] font-medium text-amber-50 transition hover:bg-amber-500/25 disabled:cursor-wait disabled:opacity-45 inline-flex items-center justify-center gap-1"
|
||||
>
|
||||
{fusionFillingProducts === "current" ? <Loader2 className="h-3 w-3 animate-spin" /> : <Upload className="h-3 w-3" />}
|
||||
填当前四角度
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void fillDesktopProductAngles("all")}
|
||||
disabled={!!fusionFillingProducts}
|
||||
className="rounded-md border border-amber-300/20 bg-amber-500/15 px-2 py-1.5 text-[10.5px] font-medium text-amber-50 transition hover:bg-amber-500/25 disabled:cursor-wait disabled:opacity-45 inline-flex items-center justify-center gap-1"
|
||||
>
|
||||
{fusionFillingProducts === "all" ? <Loader2 className="h-3 w-3 animate-spin" /> : <Upload className="h-3 w-3" />}
|
||||
填满 6 镜头
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-1.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={draftFusionDescriptions}
|
||||
className="rounded-md bg-white/10 px-2 py-1.5 text-[11px] font-medium text-white/75 transition hover:bg-white/18 hover:text-white inline-flex items-center justify-center gap-1"
|
||||
>
|
||||
<Sparkles className="h-3 w-3" />
|
||||
AI 草拟 6 条
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void runAllFusionVideos()}
|
||||
disabled={!!fusionGenerating || !onGenerateProductFusionVideo}
|
||||
className="rounded-md bg-white/10 px-2 py-1.5 text-[11px] font-medium text-white/75 transition hover:bg-white/18 hover:text-white disabled:cursor-not-allowed disabled:opacity-40 inline-flex items-center justify-center gap-1"
|
||||
>
|
||||
{fusionGenerating === "all" ? <Loader2 className="h-3 w-3 animate-spin" /> : <Sparkles className="h-3 w-3" />}
|
||||
批量排队
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
<ProductLibraryPicker
|
||||
jobId={jobId}
|
||||
compact
|
||||
buttonLabel="选用"
|
||||
title={`镜头 ${activeFusionShot + 1} 产品角度图`}
|
||||
onPick={(ref) => {
|
||||
const nextEmpty = PRODUCT_ANGLE_LABELS.findIndex((_, idx) => !currentFusionProducts[idx])
|
||||
assignFusionImage({
|
||||
shotIndex: activeFusionShot,
|
||||
slot: "product_images",
|
||||
productIndex: nextEmpty >= 0 ? nextEmpty : 0,
|
||||
}, ref)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{activeTab === "review" && (
|
||||
<section className="rounded-lg border border-white/10 bg-white/[0.035] p-2.5 text-[10.5px] leading-relaxed text-white/58">
|
||||
<div className="mb-2 text-[12px] font-semibold text-white">素材审核</div>
|
||||
|
||||
@@ -1955,6 +1955,7 @@ export function KeyframePanelNode({ data }: any) {
|
||||
embedded
|
||||
jobId={d.job.id}
|
||||
frames={d.job.frames}
|
||||
generatedVideos={d.job.generated_videos ?? []}
|
||||
activeIndex={d.expandedFrame}
|
||||
selected={d.selectedFrames}
|
||||
onClose={d.onCloseExpandedFrame}
|
||||
@@ -1964,6 +1965,7 @@ export function KeyframePanelNode({ data }: any) {
|
||||
clipboard={d.clipboard}
|
||||
onCopyImage={d.onCopyImage}
|
||||
onGenerateProductFusionVideo={d.onGenerateProductFusionVideo}
|
||||
onDeleteVideo={d.onDeleteVideo}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user