auto-save 2026-05-14 12:20 (~4)
This commit is contained in:
@@ -1,26 +1,5 @@
|
||||
{
|
||||
"entries": [
|
||||
{
|
||||
"files_changed": 1,
|
||||
"hash": "9700e2a",
|
||||
"message": "auto-save 2026-05-13 05:04 (~1)",
|
||||
"ts": "2026-05-13T05:05:02+08:00",
|
||||
"type": "commit"
|
||||
},
|
||||
{
|
||||
"files_changed": 1,
|
||||
"hash": "6e9b33b",
|
||||
"message": "auto-save 2026-05-13 05:10 (~1)",
|
||||
"ts": "2026-05-13T05:10:55+08:00",
|
||||
"type": "commit"
|
||||
},
|
||||
{
|
||||
"files_changed": 1,
|
||||
"hash": "840f833",
|
||||
"message": "auto-save 2026-05-13 05:16 (~1)",
|
||||
"ts": "2026-05-13T05:16:50+08:00",
|
||||
"type": "commit"
|
||||
},
|
||||
{
|
||||
"files_changed": 1,
|
||||
"hash": "7665d63",
|
||||
@@ -3293,6 +3272,25 @@
|
||||
"message": "auto-save 2026-05-14 12:09 (+4, ~6)",
|
||||
"hash": "04679b0",
|
||||
"files_changed": 10
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-14T12:15:27+08:00",
|
||||
"type": "commit",
|
||||
"message": "auto-save 2026-05-14 12:15 (~2)",
|
||||
"hash": "e9e0ca8",
|
||||
"files_changed": 2
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-14T04:16:10Z",
|
||||
"type": "session-heartbeat",
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 1 项未提交变更 · 最近提交:auto-save 2026-05-14 12:15 (~2)",
|
||||
"files_changed": 1
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-14T04:18:39Z",
|
||||
"type": "session-heartbeat",
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 3 项未提交变更 · 最近提交:auto-save 2026-05-14 12:15 (~2)",
|
||||
"files_changed": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
76
api/main.py
76
api/main.py
@@ -1436,16 +1436,24 @@ def _transcribe_gemini_sync(wav: Path) -> list[dict]:
|
||||
"Use English for the transcript. If exact timestamps are uncertain, return one segment "
|
||||
f"from 0 to {duration:.2f} seconds."
|
||||
)
|
||||
resp = llm().chat.completions.create(
|
||||
model=ASR_FALLBACK_MODEL,
|
||||
messages=[{"role": "user", "content": [
|
||||
{"type": "text", "text": prompt},
|
||||
{"type": "input_audio", "input_audio": {"data": audio_b64, "format": "wav"}},
|
||||
]}],
|
||||
temperature=0,
|
||||
)
|
||||
content = (resp.choices[0].message.content or "").strip()
|
||||
return _parse_asr_segments(content, duration)
|
||||
last_error: Exception | None = None
|
||||
for attempt in range(3):
|
||||
try:
|
||||
resp = llm().chat.completions.create(
|
||||
model=ASR_FALLBACK_MODEL,
|
||||
messages=[{"role": "user", "content": [
|
||||
{"type": "text", "text": prompt},
|
||||
{"type": "input_audio", "input_audio": {"data": audio_b64, "format": "wav"}},
|
||||
]}],
|
||||
temperature=0,
|
||||
)
|
||||
content = (resp.choices[0].message.content or "").strip()
|
||||
return _parse_asr_segments(content, duration)
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
if attempt < 2:
|
||||
time.sleep(1.0)
|
||||
raise last_error or RuntimeError("Gemini audio transcription failed")
|
||||
|
||||
|
||||
def _transcribe_sync(wav: Path) -> list[dict]:
|
||||
@@ -1710,7 +1718,17 @@ def pipeline_transcribe(job_id: str, manage_job_status: bool = True) -> None:
|
||||
|
||||
# 1) whisper ASR
|
||||
progress(f"{ASR_MODEL} 转录中…", 78)
|
||||
segments = _transcribe_sync(wav)
|
||||
try:
|
||||
segments = _transcribe_sync(wav)
|
||||
except Exception:
|
||||
if job.transcript:
|
||||
segments = [
|
||||
{"start": seg.start, "end": seg.end, "text": seg.en}
|
||||
for seg in job.transcript
|
||||
if seg.en.strip()
|
||||
]
|
||||
else:
|
||||
raise
|
||||
if not segments:
|
||||
raise RuntimeError("ASR 返回 0 段(可能无人声 / 格式问题)")
|
||||
|
||||
@@ -3699,12 +3717,26 @@ def create_product_fusion_guide(job_id: str, req: ProductFusionShot) -> dict:
|
||||
|
||||
def fallback_product_fusion_descriptions() -> list[str]:
|
||||
return [
|
||||
"透明骨架人双手拿起 SKG 颈部按摩仪,准备戴到脖子上,镜头轻微推近产品。",
|
||||
"透明骨架人把 SKG 按摩仪贴合到肩颈位置,手部轻轻调整两侧机身角度。",
|
||||
"透明骨架人坐在场景中轻按侧边控制区,产品保持真实比例并清晰可见。",
|
||||
"透明骨架人闭眼放松,肩颈从紧绷变舒展,产品佩戴位置稳定不漂移。",
|
||||
"镜头靠近展示 SKG 产品材质、按键和内侧触点,透明骨架人的手部不要遮挡产品主体。",
|
||||
"使用后的放松状态收尾,透明骨架人自然抬头,产品仍保持白色 U 形外观和真实比例。",
|
||||
"清晨卧室柔光里,透明骨架人把白色 SKG 颈部按摩仪轻戴到后颈,微微闭眼露出放松微笑。",
|
||||
"现代客厅沙发旁,透明骨架人双手扶住 SKG 机身两侧,肩线慢慢放低,表情从紧绷变舒适。",
|
||||
"居家办公桌前,透明骨架人轻按 SKG 侧边控制键,颈部骨架区域清晰可见,神情安静享受。",
|
||||
"暖色卧室床边,透明骨架人佩戴 SKG 后轻轻仰头,白色骨架与透明外壳干净明亮,画面高级。",
|
||||
"落地窗自然光下,透明骨架人坐姿端正,SKG 产品贴合后颈,嘴角微扬呈现轻松舒缓状态。",
|
||||
"简洁浴室镜前,透明骨架人用双手调整 SKG 贴合角度,眼神柔和,产品白色机身清楚可辨。",
|
||||
"午后阳台休息区,透明骨架人戴着 SKG 慢慢侧头伸展,肩颈线条舒展,表情舒适而不夸张。",
|
||||
"高端影棚白色背景中,透明骨架人平稳转身展示 SKG 佩戴效果,产品比例真实,轮廓清晰。",
|
||||
"健身后休息长椅上,透明骨架人把 SKG 放上肩颈,呼吸放慢,脸上出现明显放松感。",
|
||||
"办公会议间隙,透明骨架人靠在椅背上佩戴 SKG,轻轻闭眼,画面传达短暂恢复和舒适休息。",
|
||||
"夜晚卧室暖灯下,透明骨架人坐在床沿使用 SKG,肩颈骨架被柔和光线照亮,神情安稳享受。",
|
||||
"城市公寓客厅里,透明骨架人一边看向窗外一边使用 SKG,动作自然,产品贴合不漂移。",
|
||||
"极简桌面场景中,透明骨架人拿起 SKG 靠近颈部,镜头轻推展示产品材质和佩戴准备动作。",
|
||||
"木质休闲椅上,透明骨架人佩戴 SKG 后轻轻呼气,肩部下沉,脸部呈现舒缓满足的微笑。",
|
||||
"白色商业摄影场景里,透明骨架人用指尖轻触 SKG 按键,产品细节清晰,人物状态轻松专业。",
|
||||
"温暖客厅地毯旁,透明骨架人坐姿放松,SKG 稳定贴合后颈,闭眼感受舒适放松的瞬间。",
|
||||
"窗边阅读角落中,透明骨架人戴着 SKG 翻开书页,动作慢而自然,表情平和享受。",
|
||||
"办公室午休场景里,透明骨架人把 SKG 戴稳后靠回椅背,眼睛半闭,颈肩明显放松。",
|
||||
"干净产品广告场景中,透明骨架人轻扶 SKG 两端展示佩戴贴合度,微笑自然,产品不变形。",
|
||||
"收尾特写镜头里,透明骨架人佩戴 SKG 后缓慢抬头微笑,白色骨架清楚,整体干净高级。",
|
||||
]
|
||||
|
||||
|
||||
@@ -3728,8 +3760,8 @@ def generate_product_fusion_descriptions(job_id: str, req: ProductFusionDescript
|
||||
products.append(f"产品角度{len(products) + 1}未填")
|
||||
shot_lines.append(f"{i}. 首帧={first};尾帧={last};产品角度={products[0]} / {products[1]} / {products[2]} / {products[3]};已有描述={shot.action_text or '空'}")
|
||||
prompt = (
|
||||
"你是 SKG 产品短视频分镜导演。请为 6 条产品融合镜头各写一条中文动作描述,"
|
||||
"每条 20-45 字,必须说明透明骨架人在做什么、产品如何佩戴/展示、动作如何从首帧自然过渡到尾帧。"
|
||||
"你是 SKG 产品短视频分镜导演。请写 20 条中文产品融合动作描述,"
|
||||
"每条 35-70 字,必须说明透明骨架人在什么场景下使用产品、产品如何佩戴/展示、脸部如何舒适享受。"
|
||||
"产品是 SKG 白色 U 形颈部/肩颈按摩仪,四张产品角度图是同一产品的身份真源;不要写医疗治疗承诺,不要出现竞品。"
|
||||
"输出 JSON:{\"descriptions\":[\"...\", \"...\"]}。\n\n"
|
||||
+ "\n".join(shot_lines)
|
||||
@@ -3746,9 +3778,9 @@ def generate_product_fusion_descriptions(job_id: str, req: ProductFusionDescript
|
||||
text = resp.choices[0].message.content or ""
|
||||
data = json.loads(text)
|
||||
descriptions = [str(x).strip() for x in data.get("descriptions", []) if str(x).strip()]
|
||||
if len(descriptions) < 6:
|
||||
descriptions = (descriptions + fallback)[:6]
|
||||
return {"descriptions": descriptions[:6], "mode": "llm"}
|
||||
if len(descriptions) < 20:
|
||||
descriptions = (descriptions + fallback)[:20]
|
||||
return {"descriptions": descriptions[:20], "mode": "llm"}
|
||||
except Exception:
|
||||
return {"descriptions": fallback, "mode": "fallback"}
|
||||
|
||||
|
||||
@@ -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 固定模型、当前镜头状态、桌面四角度一键填充、AI 描述草稿、批量排队和产品图库选用;产品图库选中后会填入当前镜头下一个产品角度槽。主体资产页只确认一个统一主体,后端按参考重绘六张纯背景、占满画面的标准站立透明骨架人资产图;首尾帧页通过地点、风格、参考要素和可编辑 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 行镜头工作表:每行直接显示首帧、尾帧、同一产品 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>“这一组关键帧如何共同生成一个统一主体包;某张关键帧的水印、去主体场景图、产品融合镜头组和质量风险应该如何审核”。</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>为 6 行产品融合镜头生成动作描述草稿;输入重点变为首帧、尾帧和四张产品角度图,有 LLM 配置时用 <code>REWRITE_MODEL</code> 生成 JSON,无配置或失败时回退到本地镜头模板。</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>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,19 @@ SubjectAsset {
|
||||
<h2>变更记录</h2>
|
||||
<p>这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。</p>
|
||||
<div class="changelog">
|
||||
<article class="change">
|
||||
<header>
|
||||
<h3>2026-05-14 · 产品融合描述词扩成 20 条精准模板</h3>
|
||||
<span class="tag orange">产品融合</span>
|
||||
<span class="tag blue">Prompt</span>
|
||||
</header>
|
||||
<div class="body">
|
||||
<p><strong>问题:</strong>产品融合视频的动作描述不能泛泛写“人物使用产品”,需要稳定表达透明骨架人在具体场景中佩戴 SKG 产品,并呈现舒适享受状态。</p>
|
||||
<p><strong>改动:</strong>前端内置 20 条产品使用描述模板,覆盖卧室、客厅、办公、浴室、阳台、影棚、阅读角等场景;“AI 草拟 6 条”每次从 20 条中按 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>
|
||||
</article>
|
||||
<article class="change">
|
||||
<header>
|
||||
<h3>2026-05-14 · 产品融合改为首尾帧加四产品角度垫图</h3>
|
||||
|
||||
@@ -120,6 +120,28 @@ type FusionUploadTarget = {
|
||||
}
|
||||
type FusionFrameRole = "first_image" | "last_image"
|
||||
const PRODUCT_ANGLE_LABELS = ["产品角度 1", "产品角度 2", "产品角度 3", "产品角度 4"]
|
||||
const PRODUCT_FUSION_DESCRIPTION_PRESETS = [
|
||||
"清晨卧室柔光里,透明骨架人把白色 SKG 颈部按摩仪轻戴到后颈,微微闭眼露出放松微笑。",
|
||||
"现代客厅沙发旁,透明骨架人双手扶住 SKG 机身两侧,肩线慢慢放低,表情从紧绷变舒适。",
|
||||
"居家办公桌前,透明骨架人轻按 SKG 侧边控制键,颈部骨架区域清晰可见,神情安静享受。",
|
||||
"暖色卧室床边,透明骨架人佩戴 SKG 后轻轻仰头,白色骨架与透明外壳干净明亮,画面高级。",
|
||||
"落地窗自然光下,透明骨架人坐姿端正,SKG 产品贴合后颈,嘴角微扬呈现轻松舒缓状态。",
|
||||
"简洁浴室镜前,透明骨架人用双手调整 SKG 贴合角度,眼神柔和,产品白色机身清楚可辨。",
|
||||
"午后阳台休息区,透明骨架人戴着 SKG 慢慢侧头伸展,肩颈线条舒展,表情舒适而不夸张。",
|
||||
"高端影棚白色背景中,透明骨架人平稳转身展示 SKG 佩戴效果,产品比例真实,轮廓清晰。",
|
||||
"健身后休息长椅上,透明骨架人把 SKG 放上肩颈,呼吸放慢,脸上出现明显放松感。",
|
||||
"办公会议间隙,透明骨架人靠在椅背上佩戴 SKG,轻轻闭眼,画面传达短暂恢复和舒适休息。",
|
||||
"夜晚卧室暖灯下,透明骨架人坐在床沿使用 SKG,肩颈骨架被柔和光线照亮,神情安稳享受。",
|
||||
"城市公寓客厅里,透明骨架人一边看向窗外一边使用 SKG,动作自然,产品贴合不漂移。",
|
||||
"极简桌面场景中,透明骨架人拿起 SKG 靠近颈部,镜头轻推展示产品材质和佩戴准备动作。",
|
||||
"木质休闲椅上,透明骨架人佩戴 SKG 后轻轻呼气,肩部下沉,脸部呈现舒缓满足的微笑。",
|
||||
"白色商业摄影场景里,透明骨架人用指尖轻触 SKG 按键,产品细节清晰,人物状态轻松专业。",
|
||||
"温暖客厅地毯旁,透明骨架人坐姿放松,SKG 稳定贴合后颈,闭眼感受舒适放松的瞬间。",
|
||||
"窗边阅读角落中,透明骨架人戴着 SKG 翻开书页,动作慢而自然,表情平和享受。",
|
||||
"办公室午休场景里,透明骨架人把 SKG 戴稳后靠回椅背,眼睛半闭,颈肩明显放松。",
|
||||
"干净产品广告场景中,透明骨架人轻扶 SKG 两端展示佩戴贴合度,微笑自然,产品不变形。",
|
||||
"收尾特写镜头里,透明骨架人佩戴 SKG 后缓慢抬头微笑,白色骨架清楚,整体干净高级。",
|
||||
]
|
||||
|
||||
const createFusionShots = (): ProductFusionShot[] =>
|
||||
Array.from({ length: FUSION_SHOT_COUNT }, (_, i) => ({
|
||||
@@ -178,6 +200,7 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
|
||||
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
|
||||
@@ -386,28 +409,26 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
|
||||
}
|
||||
|
||||
const draftFusionDescriptions = async () => {
|
||||
const actions = [
|
||||
"透明骨架人双手拿起 SKG 颈部按摩仪,准备戴到脖子上,镜头轻微推近产品。",
|
||||
"透明骨架人把 SKG 按摩仪贴合到肩颈位置,手部轻轻调整两侧机身角度。",
|
||||
"透明骨架人坐在场景中轻按侧边控制区,产品保持真实比例并清晰可见。",
|
||||
"透明骨架人闭眼放松,肩颈从紧绷变舒展,产品佩戴位置稳定不漂移。",
|
||||
"镜头靠近展示 SKG 产品材质、按键和内侧触点,透明骨架人的手部不要遮挡产品主体。",
|
||||
"使用后的放松状态收尾,透明骨架人自然抬头,产品仍保持白色 U 形外观和真实比例。",
|
||||
]
|
||||
const actions = PRODUCT_FUSION_DESCRIPTION_PRESETS
|
||||
let descriptions = actions
|
||||
try {
|
||||
const result = await generateProductFusionDescriptions(jobId, fusionShots)
|
||||
descriptions = result.descriptions.length ? result.descriptions : actions
|
||||
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: shot.action_text?.trim() || descriptions[i] || actions[i],
|
||||
action_text: selectedDescriptions[i] || shot.action_text || actions[i],
|
||||
}))
|
||||
setFusionShots(next)
|
||||
setFusionDraftPage((prev) => prev + 1)
|
||||
void persistFusionShots(next)
|
||||
toast.success("已生成 6 条动作描述草稿,可继续手工修改")
|
||||
toast.success(`已套用 6 条动作描述 · 模板 ${start + 1}-${Math.min(start + FUSION_SHOT_COUNT, descriptions.length)}`)
|
||||
}
|
||||
|
||||
const fillDesktopProductAngles = async (scope: "current" | "all") => {
|
||||
|
||||
Reference in New Issue
Block a user