From df6f0c3bc4beaaa1b4b96d0e31c997718a647a18 Mon Sep 17 00:00:00 2001 From: kang Date: Thu, 14 May 2026 12:20:57 +0800 Subject: [PATCH] auto-save 2026-05-14 12:20 (~4) --- .memory/worklog.json | 40 ++++++++++--------- api/main.py | 76 ++++++++++++++++++++++++++----------- docs/source-analysis.html | 17 ++++++++- web/components/lightbox.tsx | 43 +++++++++++++++------ 4 files changed, 120 insertions(+), 56 deletions(-) diff --git a/.memory/worklog.json b/.memory/worklog.json index 2bab4e6..11dc02a 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -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 } ] } diff --git a/api/main.py b/api/main.py index 8576a82..0d81386 100644 --- a/api/main.py +++ b/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"} diff --git a/docs/source-analysis.html b/docs/source-analysis.html index e9162ae..76d8b7d 100644 --- a/docs/source-analysis.html +++ b/docs/source-analysis.html @@ -629,7 +629,7 @@ api/main.py
你看到的区域关键帧素材审核面板
-
主要源码FrameLightbox;按“原图/清洗、主体资产、首尾帧、产品融合、审核”五个页签组织;左侧只放主图/框选画布,但主体资产页左侧改为全部已清洗/已选参考帧网格,首尾帧页左侧显示全部关键帧并可勾选人物/机位参考。主体识别页会显示透明骨架人目标和 Vision 验收分数。清洗页右侧支持一键清洗未处理帧、单张替换清洗版和一键替换全部待应用清洗版;批量替换顺序调用 applyCleanedFrame,不新增后端接口。产品融合页左侧是纵向 6 行镜头工作表:每行直接显示首帧、尾帧、同一产品 4 个角度图、描述词、秒数和单条生成按钮,便于一次看完 6 条视频。产品融合槽位的“粘贴”优先使用应用内 clipboard,也支持选中槽位后 Cmd+V 粘贴系统图片。右侧保留 GPT Image 2 / Seedance 固定模型、当前镜头状态、桌面四角度一键填充、AI 描述草稿、批量排队和产品图库选用;产品图库选中后会填入当前镜头下一个产品角度槽。主体资产页只确认一个统一主体,后端按参考重绘六张纯背景、占满画面的标准站立透明骨架人资产图;首尾帧页通过地点、风格、参考要素和可编辑 prompt 做文字生图,生成结果写入 scene_assets 但以 asset_role=first_frame/last_frame 标记,并自动传入当前产品融合镜头。相关接口包括 cleanupFrameapplyCleanedFrameaddElementgenerateSubjectAssetsgenerateSceneAssetlistProductLibrarycopyProductLibraryAssetgenerateProductFusionDescriptions
+
主要源码FrameLightbox;按“原图/清洗、主体资产、首尾帧、产品融合、审核”五个页签组织;左侧只放主图/框选画布,但主体资产页左侧改为全部已清洗/已选参考帧网格,首尾帧页左侧显示全部关键帧并可勾选人物/机位参考。主体识别页会显示透明骨架人目标和 Vision 验收分数。清洗页右侧支持一键清洗未处理帧、单张替换清洗版和一键替换全部待应用清洗版;批量替换顺序调用 applyCleanedFrame,不新增后端接口。产品融合页左侧是纵向 6 行镜头工作表:每行直接显示首帧、尾帧、同一产品 4 个角度图、描述词、秒数和单条生成按钮,便于一次看完 6 条视频。产品融合槽位的“粘贴”优先使用应用内 clipboard,也支持选中槽位后 Cmd+V 粘贴系统图片。右侧保留 GPT Image 2 / Seedance 固定模型、当前镜头状态、桌面四角度一键填充、20 条产品使用描述模板、批量排队和产品图库选用;产品图库选中后会填入当前镜头下一个产品角度槽。主体资产页只确认一个统一主体,后端按参考重绘六张纯背景、占满画面的标准站立透明骨架人资产图;首尾帧页通过地点、风格、参考要素和可编辑 prompt 做文字生图,生成结果写入 scene_assets 但以 asset_role=first_frame/last_frame 标记,并自动传入当前产品融合镜头。相关接口包括 cleanupFrameapplyCleanedFrameaddElementgenerateSubjectAssetsgenerateSceneAssetlistProductLibrarycopyProductLibraryAssetgenerateProductFusionDescriptions
适合怎么描述“这一组关键帧如何共同生成一个统一主体包;某张关键帧的水印、去主体场景图、产品融合镜头组和质量风险应该如何审核”。
@@ -806,7 +806,7 @@ SubjectAsset { 产品图库GET /product-library/skglistProductLibrary读取内置 SKG 白底图库 manifest,返回产品标题、品类、尺寸、白底评分和预览图 URL。 产品图入库到 jobPOST /jobs/{id}/assets/product-librarycopyProductLibraryAsset把一个内置产品图库条目复制为当前 job 的普通 asset,返回 ImageRef(kind="asset"),用于画面工作台产品融合和分镜产品参考组。 产品融合引导图POST /jobs/{id}/product-fusion/guidecreateProductFusionGuide旧流程兼容接口:读取产品图和白底人物图,按 product_region 合成位置引导图。当前首尾帧流程不再主动调用它。 - 产品融合描述词POST /jobs/{id}/product-fusion/descriptionsgenerateProductFusionDescriptions为 6 行产品融合镜头生成动作描述草稿;输入重点变为首帧、尾帧和四张产品角度图,有 LLM 配置时用 REWRITE_MODEL 生成 JSON,无配置或失败时回退到本地镜头模板。 + 产品融合描述词POST /jobs/{id}/product-fusion/descriptionsgenerateProductFusionDescriptions生成 20 条产品融合动作描述库,前端每次按 6 条轮换套用到 6 行镜头;输入重点变为首帧、尾帧和四张产品角度图,有 LLM 配置时用 REWRITE_MODEL 生成 JSON,无配置或失败时回退到本地 20 条精准模板。 分镜保存PUT /frames/{idx}/storyboardupdateStoryboard保存 4 图槽、时长和改造说明。 生图POST /frames/{idx}/generategenerateImage基于关键帧或已选生成图做 image-to-image,目前可用。 @@ -917,6 +917,19 @@ SubjectAsset {

变更记录

这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。

+
+
+

2026-05-14 · 产品融合描述词扩成 20 条精准模板

+ 产品融合 + Prompt +
+
+

问题:产品融合视频的动作描述不能泛泛写“人物使用产品”,需要稳定表达透明骨架人在具体场景中佩戴 SKG 产品,并呈现舒适享受状态。

+

改动:前端内置 20 条产品使用描述模板,覆盖卧室、客厅、办公、浴室、阳台、影棚、阅读角等场景;“AI 草拟 6 条”每次从 20 条中按 6 条轮换套用,便于多次生成不同镜头组。

+

后端:generateProductFusionDescriptions 的兜底模板同步扩为 20 条,LLM 提示也改为生成 20 条 35-70 字描述,要求包含场景、佩戴/展示动作和舒适表情,同时排除医疗治疗承诺。

+

影响:api/main.pyweb/components/lightbox.tsxdocs/source-analysis.html

+
+

2026-05-14 · 产品融合改为首尾帧加四产品角度垫图

diff --git a/web/components/lightbox.tsx b/web/components/lightbox.tsx index 9c3862f..9cdaba4 100644 --- a/web/components/lightbox.tsx +++ b/web/components/lightbox.tsx @@ -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(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") => {