auto-save 2026-05-14 12:48 (~4)
This commit is contained in:
@@ -1,19 +1,5 @@
|
||||
{
|
||||
"entries": [
|
||||
{
|
||||
"files_changed": 1,
|
||||
"hash": "169951b",
|
||||
"message": "auto-save 2026-05-13 06:09 (~1)",
|
||||
"ts": "2026-05-13T06:09:56+08:00",
|
||||
"type": "commit"
|
||||
},
|
||||
{
|
||||
"files_changed": 1,
|
||||
"hash": "d0b73fd",
|
||||
"message": "auto-save 2026-05-13 06:15 (~1)",
|
||||
"ts": "2026-05-13T06:15:50+08:00",
|
||||
"type": "commit"
|
||||
},
|
||||
{
|
||||
"files_changed": 1,
|
||||
"hash": "1dd2c67",
|
||||
@@ -3287,6 +3273,19 @@
|
||||
"type": "session-heartbeat",
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 1 项未提交变更 · 最近提交:auto-save 2026-05-14 12:37 (~6)",
|
||||
"files_changed": 1
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-14T12:43:03+08:00",
|
||||
"type": "commit",
|
||||
"message": "auto-save 2026-05-14 12:42 (~9)",
|
||||
"hash": "2d1a89f",
|
||||
"files_changed": 9
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-14T04:46:11Z",
|
||||
"type": "session-heartbeat",
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 4 项未提交变更 · 最近提交:auto-save 2026-05-14 12:42 (~9)",
|
||||
"files_changed": 4
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -556,8 +556,8 @@
|
||||
<div class="step"><div class="num">3</div><h3>清洗水印</h3><p>对关键帧做全图或区域清洗,清洗版先进入待审核状态;确认后可单张替换,也可一键替换全部待应用清洗版。</p></div>
|
||||
<div class="step"><div class="num">4</div><h3>主体识别</h3><p>识别场景和主体候选,只是候选,不应锁死。</p></div>
|
||||
<div class="step"><div class="num">5</div><h3>素材准备</h3><p>清洗关键帧,把多张关键帧作为同一主体的参考,先重绘六张标准站立主体资产图,再按关键帧生成多个去主体、相似或换风格场景图。</p></div>
|
||||
<div class="step"><div class="num">6</div><h3>分镜改造</h3><p>把参考主体、场景、动作和 SKG 产品放入分镜结构;产品融合使用纵向 6 行镜头工作表,每行绑定产品图、白底人物图、产品区域、场景图、描述词、秒数和单条生成入口。</p></div>
|
||||
<div class="step"><div class="num">7</div><h3>生成视频</h3><p>普通分镜可调用 Seedance / Kling / Veo 3;产品融合固定用 GPT Image 2 生成位置引导图,再用 Seedance 按秒数生成视频,结果回写到画面工作台节点。</p></div>
|
||||
<div class="step"><div class="num">6</div><h3>分镜改造</h3><p>把参考主体、场景、动作和 SKG 产品放入分镜结构;产品融合使用纵向 6 行镜头工作表,只补人物首帧、尾帧、描述词和秒数,产品图固定内置。</p></div>
|
||||
<div class="step"><div class="num">7</div><h3>生成视频</h3><p>普通分镜可调用 Seedance / Kling / Veo 3;产品融合自动传入固定 4 张 SKG 产品图和每行首尾帧,用 Seedance 按秒数生成视频,结果回写到对应行。</p></div>
|
||||
<div class="step"><div class="num">8</div><h3>声音文案</h3><p>音频轨独立处理:提取原音频并按实际秒数生成 SKG 英文产品介绍 voice-over,ASR/翻译只作为改前对照和节奏参考;配置 MiniMax 后从男声、女声、成熟声池随机生成自然英文配音 mp3。底部音频条播放原音频时,指针会按时间走过字幕节点。</p></div>
|
||||
<div class="step"><div class="num">9</div><h3>合成成品</h3><p>片段、字幕、配音、转场合成最终 mp4。当前未实现。</p></div>
|
||||
</div>
|
||||
@@ -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 行镜头工作表:每行只显示首帧、尾帧、已预填动作描述、秒数、生成按钮和对应视频结果;四张桌面 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><code>FrameLightbox</code>;按“原图/清洗、主体资产、首尾帧、产品融合、审核”五个页签组织;左侧只放主图/框选画布,但主体资产页左侧改为全部已清洗/已选参考帧网格,首尾帧页左侧显示全部关键帧并可勾选人物/机位参考。主体识别页会显示透明骨架人目标和 Vision 验收分数。清洗页右侧支持一键清洗未处理帧、单张替换清洗版和一键替换全部待应用清洗版;批量替换顺序调用 <code>applyCleanedFrame</code>,不新增后端接口。产品融合页左侧是纵向 6 行镜头工作表:每行只显示首帧、尾帧、已预填动作描述、秒数、生成按钮和对应视频结果;描述词内置 36 条镜头语言模板,按“建立出场、产品入画、佩戴贴合、使用感受、生活延展、收尾记忆”排列,点击“换一组”只刷新 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">
|
||||
@@ -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 条产品融合动作描述库。当前前端默认直接用本地 20 条精准模板预填 6 行镜头,不再显示单独的 AI 草拟入口。</td></tr>
|
||||
<tr><td>产品融合描述词</td><td><code>POST /jobs/{id}/product-fusion/descriptions</code></td><td><code>generateProductFusionDescriptions</code></td><td>兼容接口:可生成产品融合动作描述库。当前前端默认直接用本地 36 条镜头语言模板预填 6 行镜头,并通过“换一组”按钮按 6 条一组轮换。</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>
|
||||
@@ -917,6 +917,18 @@ SubjectAsset {
|
||||
<h2>变更记录</h2>
|
||||
<p>这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。</p>
|
||||
<div class="changelog">
|
||||
<article class="change">
|
||||
<header>
|
||||
<h3>2026-05-14 · 音频提取直接生成英文产品口播</h3>
|
||||
<span class="tag gray">Audio</span>
|
||||
<span class="tag green">MiniMax</span>
|
||||
</header>
|
||||
<div class="body">
|
||||
<p><strong>问题:</strong>“提取音频”不能只做原音频转文字再改写,用户需要点击后直接得到介绍 SKG 产品的英文文案和配音,长度尽量贴近原音频,并且声音不能生硬。</p>
|
||||
<p><strong>改动:</strong><code>pipeline_transcribe</code> 提取 <code>audio.wav</code> 后读取原音频时长,用该时长估算英文口播词数;<code>_rewrite_audio_script_sync</code> 改为生成自然、有趣、可直接 TTS 的 SKG 英文产品介绍。ASR/翻译保留为对照和节奏参考,ASR 不可用时仍继续生成产品口播。MiniMax voice_id 改为从 <code>MINIMAX_TTS_VOICE_POOL</code> 随机选择男声、女声或成熟声。</p>
|
||||
<p><strong>影响:</strong><code>api/main.py</code>、<code>api/.env.example</code>、<code>api/README.md</code>、<code>RULES.md</code>、<code>web/components/nodes/index.tsx</code>、<code>web/components/audio-strip.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 · 产品融合描述词扩成 20 条精准模板</h3>
|
||||
@@ -1042,13 +1054,13 @@ SubjectAsset {
|
||||
</article>
|
||||
<article class="change">
|
||||
<header>
|
||||
<h3>2026-05-14 · 音频处理接入 SKG 英文口播改写与 MiniMax 配音</h3>
|
||||
<h3>2026-05-14 · 音频处理接入 SKG 英文产品口播与 MiniMax 配音</h3>
|
||||
<span class="tag gray">Audio</span>
|
||||
<span class="tag green">MiniMax</span>
|
||||
</header>
|
||||
<div class="body">
|
||||
<p><strong>问题:</strong>音频处理节点之前只说明“音轨 → ASR → 翻译 → 改写”,没有真实改写产物,也没有配音输出;用户无法直接拿到符合 SKG 产品语境的英文口播。</p>
|
||||
<p><strong>改动:</strong><code>Job</code> 新增 <code>audio_script</code>,<code>pipeline_transcribe</code> 在 ASR 和翻译后生成 SKG 英文改写文案,并在配置 <code>MINIMAX_API_KEY</code> 时调用 MiniMax T2A 输出 <code>/jobs/{id}/audio-script.mp3</code>。前端 <code>AudioNode</code> 和侧栏 Rewrite 区显示模型链路、英文改写文案和配音播放器。</p>
|
||||
<p><strong>问题:</strong>音频处理节点之前只说明“音轨 → ASR → 翻译 → 改写”,没有按原音频时长生成的产品介绍产物,也没有配音输出;用户无法直接拿到符合 SKG 产品语境的英文口播。</p>
|
||||
<p><strong>改动:</strong><code>Job</code> 新增 <code>audio_script</code>,<code>pipeline_transcribe</code> 提取 <code>audio.wav</code> 后按原音频秒数生成 SKG 英文产品介绍文案,并在配置 <code>MINIMAX_API_KEY</code> 时调用 MiniMax T2A 输出 <code>/jobs/{id}/audio-script.mp3</code>。MiniMax voice_id 从英文男声、女声、成熟声池随机选择;前端 <code>AudioNode</code> 和侧栏 Rewrite 区显示模型链路、英文产品文案和配音播放器。</p>
|
||||
<p><strong>边界:</strong>MiniMax 官方 Speech API 当前接入的是 TTS 配音,不替代 ASR;原始音频文案提取仍走现有 OpenAI-compatible audio transcription 入口。</p>
|
||||
<p><strong>影响:</strong><code>api/main.py</code>、<code>api/.env.example</code>、<code>api/README.md</code>、<code>web/lib/api.ts</code>、<code>web/components/nodes/index.tsx</code>、<code>web/components/dashboard.tsx</code>、<code>web/app/page.tsx</code>、<code>docs/source-analysis.html</code>。</p>
|
||||
</div>
|
||||
|
||||
@@ -539,9 +539,12 @@ export default function Home() {
|
||||
`产品角度图 2:${labelOf(productRefs[1], "SKG 产品侧面/斜侧视角")}。`,
|
||||
`产品角度图 3:${labelOf(productRefs[2], "SKG 产品背面/细节视角")}。`,
|
||||
`产品角度图 4:${labelOf(productRefs[3], "SKG 产品补充/底部或佩戴视角")}。`,
|
||||
"产品使用部位:这是颈部/肩颈按摩仪,只能自然佩戴或贴合在脖子、后颈、颈肩交界处;不要放到手臂、腰、腿、胸口、眼部或背景里。",
|
||||
"比例尺寸:产品应符合真实颈部按摩仪大小,U 形结构环绕后颈但不能巨大化、缩小成饰品、嵌入身体、悬浮或穿透透明人体。",
|
||||
"镜头语言:严格按动作描述里的出场方式、景别、运镜、产品进入方式、佩戴贴合动作和收尾方式执行。",
|
||||
`动作描述:${shot.action_text.trim()}`,
|
||||
TRANSPARENT_HUMAN_VIDEO_PROMPT,
|
||||
"融合要求:产品必须自然出现在透明骨架人动作中,尺寸可信,透视一致,贴合身体/手部/使用区域,不能悬浮、漂移、融化、扭曲或变成其他物体。",
|
||||
"融合要求:产品必须自然出现在透明骨架人动作中,尺寸可信,透视一致,只贴合手部拿取和后颈/颈肩使用区域,不能悬浮、漂移、融化、扭曲或变成其他物体。",
|
||||
"首尾连续性:镜头从首帧自然运动到尾帧,中间不要跳切,不换角色,不换产品,不突然改变场景。",
|
||||
"产品一致性:严格保持 SKG 产品外观、颜色、材质、U 形结构、按摩触点、按键和比例;四张产品角度图是产品身份真源。",
|
||||
"场景要求:背景、空间、光线和阴影要自然统一,不要出现水印、平台 UI、字幕或竞品包装。",
|
||||
|
||||
@@ -120,7 +120,7 @@ type FusionUploadTarget = {
|
||||
}
|
||||
type FusionFrameRole = "first_image" | "last_image"
|
||||
const FUSION_PROMPT_MARKER_PREFIX = "产品融合镜头ID:"
|
||||
const PRODUCT_FUSION_DESCRIPTION_PRESETS = [
|
||||
const LEGACY_PRODUCT_FUSION_DESCRIPTION_PRESETS = [
|
||||
"清晨卧室柔光里,透明骨架人把白色 SKG 颈部按摩仪轻戴到后颈,微微闭眼露出放松微笑。",
|
||||
"现代客厅沙发旁,透明骨架人双手扶住 SKG 机身两侧,肩线慢慢放低,表情从紧绷变舒适。",
|
||||
"居家办公桌前,透明骨架人轻按 SKG 侧边控制键,颈部骨架区域清晰可见,神情安静享受。",
|
||||
@@ -143,6 +143,60 @@ const PRODUCT_FUSION_DESCRIPTION_PRESETS = [
|
||||
"收尾特写镜头里,透明骨架人佩戴 SKG 后缓慢抬头微笑,白色骨架清楚,整体干净高级。",
|
||||
]
|
||||
|
||||
const PRODUCT_FUSION_LENS_STAGES = [
|
||||
"01 建立出场",
|
||||
"02 产品入画",
|
||||
"03 佩戴贴合",
|
||||
"04 使用感受",
|
||||
"05 生活延展",
|
||||
"06 收尾记忆",
|
||||
]
|
||||
|
||||
const PRODUCT_FUSION_DESCRIPTION_PRESETS = [
|
||||
"镜头01|建立出场|半身中景,透明骨架人先自然出现在清晨卧室柔光里,SKG 白色产品放在桌面或手边;镜头慢慢推近,让人物透明外壳和白色骨架先成立,结尾手准备伸向产品。",
|
||||
"镜头02|产品入画|从桌面产品近景开始,透明骨架人的手把 SKG 产品拿起带入画面;镜头从产品轻移到人物肩颈,产品尺寸真实,不能漂浮,结尾靠近后颈。",
|
||||
"镜头03|佩戴贴合|肩颈侧面近景,透明骨架人双手扶住 SKG 两端贴合后颈并微调角度;镜头轻微环绕展示 U 形结构、触点和颈椎位置,结尾产品稳定贴合。",
|
||||
"镜头04|使用感受|半身近景,产品已佩戴,透明骨架人闭眼呼吸放慢、肩线下沉、嘴角微笑;镜头慢推,不换场景,突出舒适享受但不要医疗治疗暗示。",
|
||||
"镜头05|生活延展|现代客厅、办公桌或窗边休息场景,透明骨架人保持佩戴 SKG 做轻松阅读或休息动作;镜头横移或轻绕,产品位置稳定,人物透明身体和骨架清晰。",
|
||||
"镜头06|收尾记忆|产品和人物肩颈半身特写,透明骨架人缓慢抬头微笑定格,SKG 产品轮廓清楚可辨;镜头停在干净高级的广告收尾,不出现文字和 logo 字幕。",
|
||||
"备用01|镜中出场|浴室或卧室镜前,透明骨架人先在镜面中出现,手边放着 SKG 产品;镜头从镜中人物拉到真实人物,最后拿起产品准备佩戴。",
|
||||
"备用02|手部带入|产品先在白色桌面上清楚出现,透明骨架人的手进入画面拿起 SKG;镜头跟随手部移动到肩颈区域,强调产品被真实拿起而不是凭空出现。",
|
||||
"备用03|侧面贴合|45 度侧面近景,透明骨架人把 SKG 产品从颈侧滑入正确位置,轻轻按压贴合;镜头短距离环绕,确保产品透视、比例和身体接触真实。",
|
||||
"备用04|按键反馈|产品佩戴后,透明骨架人用指尖轻按侧边按键,肩颈骨架区域被柔和光线照亮;镜头从按键细节回到人物放松表情。",
|
||||
"备用05|办公舒缓|居家办公桌前,透明骨架人佩戴 SKG 靠回椅背,手从键盘移开,肩部慢慢放松;镜头从电脑桌面横移到人物半身。",
|
||||
"备用06|沙发休息|现代客厅沙发上,透明骨架人戴着 SKG 闭眼休息,呼吸节奏变慢;镜头轻微推近产品和肩颈,最后停在舒适表情。",
|
||||
"备用07|窗边阅读|窗边阅读角中,透明骨架人一边翻书一边稳定佩戴 SKG;镜头从书页过渡到产品,再到透明骨架人的平和微笑。",
|
||||
"备用08|影棚展示|高端白色影棚里,透明骨架人佩戴 SKG 缓慢转身展示正面和侧面贴合效果;镜头平稳环绕,产品外观不能变形。",
|
||||
"备用09|床边放松|暖色卧室床边,透明骨架人坐下后把 SKG 戴稳,肩线下沉,脸部从疲惫转为舒适;镜头慢慢推近收住。",
|
||||
"备用10|阳台伸展|午后阳台休息区,透明骨架人戴着 SKG 缓慢侧头伸展,颈椎白骨清楚可见;镜头横移跟随动作,产品不漂移。",
|
||||
"备用11|产品特写转人|开场为 SKG 产品白底特写,随后自然切到透明骨架人佩戴后的半身画面;镜头语言干净商业,强调产品身份一致。",
|
||||
"备用12|拿起到佩戴一镜到底|透明骨架人从桌面拿起 SKG,抬手、对准后颈、轻轻戴上,一镜到底完成动作;产品始终保持真实尺寸和方向。",
|
||||
"备用13|舒适反应特写|肩颈近景转脸部特写,透明骨架人闭眼微笑,骨架和透明外壳保持同一角色;镜头稳定,不夸张表演。",
|
||||
"备用14|最终定格|透明骨架人佩戴 SKG 面向镜头轻轻微笑,产品清晰贴合后颈,背景干净高级;最后 1 秒稳定定格作为广告收尾。",
|
||||
"备用15|门口入场|透明骨架人从现代公寓门口走入画面,肩颈略显紧绷,SKG 产品放在玄关台面;镜头中景跟随,结尾视线落到产品。",
|
||||
"备用16|从包中取出|透明骨架人坐到办公椅上,从随身包里拿出白色 SKG 产品;镜头从包内产品切到人物手部,强调产品自然进入生活场景。",
|
||||
"备用17|正面佩戴|正面半身镜头,透明骨架人双手把 SKG 从胸前抬到后颈,动作慢而准确;产品贴合后颈时停顿,比例和接触关系真实。",
|
||||
"备用18|颈肩舒展|产品已佩戴,透明骨架人缓慢转动脖颈、肩部下沉,脸部露出轻松表情;镜头轻推近肩颈,不出现夸张疗效表达。",
|
||||
"备用19|移动生活镜头|透明骨架人佩戴 SKG 在客厅和窗边之间轻松移动,透明身体和白色骨架始终清楚;镜头平稳横移,产品位置不变。",
|
||||
"备用20|产品轮廓收束|收尾以肩颈侧面特写呈现 SKG 轮廓,透明骨架人微笑停住,背景柔和虚化;画面干净高级,无文字和水印。",
|
||||
"备用21|影棚人物建立|高端白色影棚中,透明骨架人站立转向镜头,白色骨架清晰可见,SKG 产品置于旁边展台;镜头缓慢推近建立商业感。",
|
||||
"备用22|产品旋转展示|白底产品图感的 SKG 产品在人物手中被轻轻转动展示正侧面,随后靠近透明骨架人的后颈;镜头跟随产品,不让产品变形。",
|
||||
"备用23|侧后方落位|从人物侧后方观察 SKG 落到后颈位置,透明皮肤包裹白色颈椎骨架,双手轻调两端;镜头短距离环绕确认贴合。",
|
||||
"备用24|安静闭眼|透明骨架人佩戴后闭眼停留,呼吸放慢、肩部放松,产品与颈部阴影自然;镜头不切换,只做细微推进。",
|
||||
"备用25|场景转为日常|镜头从肩颈近景拉开到完整生活场景,人物继续佩戴 SKG 阅读、休息或看窗外;产品清楚但不抢走人物主体。",
|
||||
"备用26|广告式收束|透明骨架人面对镜头轻轻微笑,手放下不再遮挡产品,SKG 稳定贴合后颈;镜头保持半身构图作为干净收尾。",
|
||||
"备用27|疲惫到放松|开场透明骨架人坐在办公桌前揉肩,桌面有 SKG 产品;镜头从疲惫状态慢慢推近,结尾人物准备拿起产品。",
|
||||
"备用28|桌面到肩颈|产品从桌面被拿起,镜头沿手部轨迹移动到肩颈,透明骨架人的颈椎和肋骨清晰;产品必须跟随手部运动。",
|
||||
"备用29|双手校准|透明骨架人用双手对称扶住 SKG 两端,微调到后颈正确位置;镜头正侧之间轻微移动,强调贴合和尺寸可信。",
|
||||
"备用30|舒适微表情|佩戴稳定后,透明骨架人眼神变柔和、嘴角轻扬,肩颈线条放松;镜头从产品细节回到脸部,保持广告级质感。",
|
||||
]
|
||||
|
||||
const legacyFusionDescriptionSet = new Set(LEGACY_PRODUCT_FUSION_DESCRIPTION_PRESETS)
|
||||
const shouldUseDefaultFusionDescription = (value?: string | null) => {
|
||||
const text = value?.trim()
|
||||
return !text || legacyFusionDescriptionSet.has(text)
|
||||
}
|
||||
|
||||
const createFusionShots = (): ProductFusionShot[] =>
|
||||
Array.from({ length: FUSION_SHOT_COUNT }, (_, i) => ({
|
||||
id: `shot-${i + 1}`,
|
||||
@@ -169,7 +223,7 @@ const normalizeFusionShots = (shots?: ProductFusionShot[] | null): ProductFusion
|
||||
...item,
|
||||
...shot,
|
||||
product_images: shot.product_images?.slice(0, PRODUCT_ANGLE_COUNT) ?? [],
|
||||
action_text: shot.action_text?.trim() || item.action_text,
|
||||
action_text: shouldUseDefaultFusionDescription(shot.action_text) ? item.action_text : shot.action_text,
|
||||
id: shot.id || item.id,
|
||||
}
|
||||
})
|
||||
@@ -200,6 +254,7 @@ 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 [fusionPresetPage, setFusionPresetPage] = useState(0)
|
||||
const [editingElement, setEditingElement] = useState<{
|
||||
frameIndex: number
|
||||
id: string
|
||||
@@ -415,6 +470,19 @@ export function FrameLightbox({ jobId, frames, generatedVideos = [], activeIndex
|
||||
}
|
||||
}
|
||||
|
||||
const rotateFusionDescriptions = () => {
|
||||
const page = fusionPresetPage + 1
|
||||
const start = (page * FUSION_SHOT_COUNT) % PRODUCT_FUSION_DESCRIPTION_PRESETS.length
|
||||
const next = fusionShots.map((shot, i) => ({
|
||||
...shot,
|
||||
action_text: PRODUCT_FUSION_DESCRIPTION_PRESETS[(start + i) % PRODUCT_FUSION_DESCRIPTION_PRESETS.length] || shot.action_text,
|
||||
}))
|
||||
setFusionPresetPage(page)
|
||||
setFusionShots(next)
|
||||
void persistFusionShots(next)
|
||||
toast.success(`已换第 ${Math.floor(start / FUSION_SHOT_COUNT) + 1} 组镜头语言`)
|
||||
}
|
||||
|
||||
const runFusionVideo = async (index: number) => {
|
||||
const shot = fusionShots[index]
|
||||
if (!shot?.first_image || !shot.last_image || !shot.action_text?.trim()) {
|
||||
@@ -989,6 +1057,16 @@ export function FrameLightbox({ jobId, frames, generatedVideos = [], activeIndex
|
||||
<span className="rounded bg-black/35 px-1.5 py-0.5 text-[9.5px] font-mono text-white/55">
|
||||
{fusionReadyCount}/6 可生成
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={rotateFusionDescriptions}
|
||||
disabled={!!fusionGenerating}
|
||||
className="inline-flex h-6 items-center justify-center gap-1 rounded bg-white/10 px-2 text-[9.5px] font-medium text-white/65 transition hover:bg-white/18 hover:text-white disabled:cursor-not-allowed disabled:opacity-40"
|
||||
title="换一组内置镜头语言,不改变首帧和尾帧"
|
||||
>
|
||||
<RefreshCw className="h-3 w-3" />
|
||||
换一组
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void runAllFusionVideos()}
|
||||
@@ -1014,6 +1092,7 @@ export function FrameLightbox({ jobId, frames, generatedVideos = [], activeIndex
|
||||
const latestVideoUrl = latestShotVideo?.url ? apiAssetUrl(latestShotVideo.url) : ""
|
||||
const ready = !!(shot.first_image && shot.last_image && shot.action_text?.trim())
|
||||
const busy = fusionGenerating === i || fusionGenerating === "all"
|
||||
const lensStageLabel = PRODUCT_FUSION_LENS_STAGES[i] ?? `镜头 ${i + 1}`
|
||||
const pasteIntoSlot = (target: FusionUploadTarget, label: string) => {
|
||||
setActiveFusionShot(i)
|
||||
if (clipboard) {
|
||||
@@ -1146,7 +1225,7 @@ export function FrameLightbox({ jobId, frames, generatedVideos = [], activeIndex
|
||||
|
||||
<label className="block">
|
||||
<div className="mb-1 flex items-center justify-between gap-2">
|
||||
<span className="text-[9px] text-white/38">描述词 · 人在干什么</span>
|
||||
<span className="text-[9px] text-amber-100/65">{lensStageLabel}</span>
|
||||
<span className="text-[8.5px] text-white/30">#{i + 1}</span>
|
||||
</div>
|
||||
<textarea
|
||||
|
||||
Reference in New Issue
Block a user