auto-save 2026-05-14 06:38 (~3)

This commit is contained in:
2026-05-14 06:39:09 +08:00
parent 0d86b4cff2
commit 27a3da4b3a
3 changed files with 205 additions and 34 deletions

View File

@@ -1,19 +1,5 @@
{
"entries": [
{
"files_changed": 1,
"hash": "440164e",
"message": "auto-save 2026-05-12 17:50 (~1)",
"ts": "2026-05-12T17:51:03+08:00",
"type": "commit"
},
{
"files_changed": 2,
"hash": "aa5ad08",
"message": "auto-save 2026-05-12 18:29 (+1, ~1)",
"ts": "2026-05-12T18:29:59+08:00",
"type": "commit"
},
{
"files_changed": 3,
"hash": "64db093",
@@ -3348,6 +3334,19 @@
"type": "session-heartbeat",
"message": "Claude 会话活跃 · 最近命令claude · 5 项未提交变更 · 最近提交auto-save 2026-05-14 06:27 (~4)",
"files_changed": 5
},
{
"ts": "2026-05-14T06:33:37+08:00",
"type": "commit",
"message": "auto-save 2026-05-14 06:33 (~5)",
"hash": "0d86b4c",
"files_changed": 5
},
{
"ts": "2026-05-13T22:38:51Z",
"type": "session-heartbeat",
"message": "Codex 会话活跃 · 最近命令codex · 3 项未提交变更 · 最近提交auto-save 2026-05-14 06:33 (~5)",
"files_changed": 3
}
]
}

View File

@@ -623,7 +623,7 @@ api/main.py
</div>
<div class="flow-row">
<div><strong>你看到的区域</strong><span>关键帧素材审核面板</span></div>
<div><strong>主要源码</strong><span><code>FrameLightbox</code>;按“原图/清洗、主体资产、场景图、审核”四个页签组织;左侧只放主图/框选画布,但主体资产页左侧改为全部参考帧网格;右侧承载当前页操作、状态和结果主体资产页只确认一个统一主体,默认用全部关键帧或已选关键帧作为参考,后端按参考重绘六张纯背景、占满画面的标准站立主体图;场景图依赖主体资产,按当前关键帧生成去主体原场景、相似新场景或同构换风格。相关接口包括 <code>cleanupFrame</code><code>addElement</code><code>generateSubjectAssets</code><code>generateSceneAsset</code></span></div>
<div><strong>主要源码</strong><span><code>FrameLightbox</code>;按“原图/清洗、主体资产、场景图、审核”四个页签组织;左侧只放主图/框选画布,但主体资产页左侧改为全部已清洗/已选参考帧网格,场景图页左侧显示全部关键帧并可勾选场景参考;右侧承载当前页操作、状态和结果主体资产页只确认一个统一主体,后端按参考重绘六张纯背景、占满画面的标准站立主体图;场景图依赖主体资产,右侧通过地点、生成方式、风格和参考要素拼出可编辑 prompt按当前关键帧生成去主体原场景、相似新场景或同构换风格。相关接口包括 <code>cleanupFrame</code><code>addElement</code><code>generateSubjectAssets</code><code>generateSceneAsset</code></span></div>
<div><strong>适合怎么描述</strong><span>“这一组关键帧如何共同生成一个统一主体包;某张关键帧的水印、去主体场景图和质量风险应该如何审核”。</span></div>
</div>
<div class="flow-row">
@@ -731,7 +731,7 @@ SubjectAsset {
<tr><td>元素增改删</td><td><code>POST/PATCH/DELETE /elements</code></td><td><code>addElement/updateElement/deleteElement</code></td><td>让用户修正 Vision 错误,避免候选结果锁死。</td></tr>
<tr><td>元素提取</td><td><code>POST /elements/{element_id}/cutout</code></td><td><code>cutoutElement</code></td><td>调用图像模型生成独立白底素材图,每次累积一张 cutout。</td></tr>
<tr><td>主体资产包</td><td><code>POST /elements/{element_id}/subject-assets</code></td><td><code>generateSubjectAssets</code></td><td>根据参考帧重新绘制一个统一主体资产包;前端默认把全部关键帧作为 <code>source_frame_indices</code>,如果用户手动选择了关键帧则只传已选帧,后端拼参考板。默认输出六张标准站立/转身参考图,纯白/黑背景,不含其他元素,并裁去空白让主体占满画面。</td></tr>
<tr><td>场景资产</td><td><code>POST /frames/{idx}/scene-asset</code></td><td><code>generateSceneAsset</code></td><td>在统一主体资产之后,按当前关键帧生成去主体背景板;请求包含 <code>scene_mode</code><code>scene_style</code>,可做原场景补背景、相似新场景或同构换风格,保留历史版本用于人工审核。</td></tr>
<tr><td>场景资产</td><td><code>POST /frames/{idx}/scene-asset</code></td><td><code>generateSceneAsset</code></td><td>在统一主体资产之后,按当前关键帧生成去主体背景板;请求包含 <code>scene_mode</code><code>scene_style</code><code>prompt</code><code>source_frame_indices</code>,可用左侧选择的参考帧 + 右侧关键词生成原场景补背景、相似新场景或同构换风格,保留历史版本用于人工审核。</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>
@@ -841,6 +841,19 @@ SubjectAsset {
<h2>变更记录</h2>
<p>这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。</p>
<div class="changelog">
<article class="change">
<header>
<h3>2026-05-14 · 场景图改为全图参考和关键词 Prompt</h3>
<span class="tag violet">FrameLightbox</span>
<span class="tag blue">Prompt</span>
</header>
<div class="body">
<p><strong>问题:</strong>场景图页不能只围绕当前单张图;它需要看到全部关键帧,并通过地点、风格、参考要素等关键词组合出可控 prompt再生成场景。</p>
<p><strong>改动:</strong><code>FrameLightbox</code> 的“场景图”页左侧改为全部关键帧网格:点击图片设为生成目标,点击“选”加入场景参考。右侧新增地点、生成方式、风格、额外关键词和参考要素 chips并自动拼出可编辑场景 prompt。</p>
<p><strong>后端:</strong><code>generateSceneAsset</code> 请求新增 <code>prompt</code><code>source_frame_indices</code>;多张参考帧会拼成 contact sheet 给图像模型,同时把用户 prompt 注入场景生成提示词。</p>
<p><strong>影响:</strong><code>web/components/lightbox.tsx</code><code>web/lib/api.ts</code><code>api/main.py</code><code>docs/source-analysis.html</code></p>
</div>
</article>
<article class="change">
<header>
<h3>2026-05-14 · 主体资产改为参考重绘六张标准图</h3>
@@ -849,7 +862,7 @@ SubjectAsset {
</header>
<div class="body">
<p><strong>问题:</strong>主体资产不是抠图,也不是只看当前单帧生成多角度;主体页需要看到全部参考帧,并用这些参考重新绘制一个完整主体。</p>
<p><strong>改动:</strong><code>FrameLightbox</code> 在“主体资产”页左侧显示全部参考帧网格,小图排列,可点击切换当前帧右侧仍负责统一主体确认和生成。人物/生物默认视图改为六张标准站立/转身图:正面、背面、左侧、右侧、左前 45°、右前 45°。</p>
<p><strong>改动:</strong><code>FrameLightbox</code> 在“主体资产”页左侧显示参考帧网格,优先纳入所有已清洗帧,额外已选帧也会并入;小图排列,可点击切换当前帧右侧仍负责统一主体确认和生成。人物/生物默认视图改为六张标准站立/转身图:正面、背面、左侧、右侧、左前 45°、右前 45°。</p>
<p><strong>后端:</strong><code>generateSubjectAssets</code> prompt 改为“参考重绘”,明确禁止裁剪/抠图/粘贴源像素,要求主体完整居中、纯白/黑背景、无其他元素,并占画面约 85-95% 高度;落盘时会裁掉纯背景空白并放大主体。</p>
<p><strong>影响:</strong><code>web/components/lightbox.tsx</code><code>web/components/nodes/index.tsx</code><code>api/main.py</code><code>docs/source-analysis.html</code></p>
</div>

View File

@@ -42,6 +42,27 @@ const LIVING_VIEW_OPTIONS = [
["three_quarter_right", "右前 45°"],
]
const LIVING_EXPRESSION_OPTIONS = [
["expression_neutral", "中性脸"],
["expression_smile", "微笑"],
["expression_happy", "开心"],
["expression_serious", "严肃"],
["expression_surprised", "惊讶"],
]
const LIVING_ACTION_OPTIONS = [
["action_walk", "走路"],
["action_turn", "转身"],
["action_hold", "手持"],
["action_use", "使用"],
]
const LIVING_VIEW_GROUPS = [
{ title: "身份标准图", hint: "默认必出 · 用来锁定长相、体型、服装和比例", options: LIVING_VIEW_OPTIONS },
{ title: "表情补充", hint: "需要口播、反应或情绪镜头时再勾", options: LIVING_EXPRESSION_OPTIONS },
{ title: "动作补充", hint: "需要动作镜头时再勾,仍保持同一人物身份", options: LIVING_ACTION_OPTIONS },
]
type LightboxTab = "clean" | "scene" | "subject" | "review"
const LIGHTBOX_TABS: Array<{ key: LightboxTab; label: string }> = [
@@ -550,6 +571,8 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
className="flex flex-col items-stretch gap-2 overflow-y-auto pr-1"
style={isSubjectTab
? { flex: "1 1 360px", minWidth: 220, maxWidth: 460, minHeight: 0 }
: isSceneTab
? { flex: "1 1 430px", minWidth: 280, maxWidth: 560, minHeight: 0 }
: isCleanTab
? { flex: "1 1 500px", minWidth: 300, maxWidth: 600, minHeight: 0 }
: { flex: "1 1 560px", minWidth: 300, maxWidth: 680, minHeight: 0 }}
@@ -606,6 +629,76 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
</div>
</section>
) : isSceneTab ? (
<section className="rounded-lg border border-emerald-300/15 bg-emerald-500/[0.06] p-2.5">
<div className="mb-2 flex items-center justify-between gap-2">
<div className="text-[12px] font-semibold text-white"></div>
<span className="text-[9.5px] font-mono text-white/38">
{selectedFrameIndices.length > 0 ? `${selectedFrameIndices.length} 已选参考` : "默认当前帧"}
</span>
</div>
<div
className="grid gap-2"
style={{ gridTemplateColumns: "repeat(auto-fill, minmax(104px, 1fr))" }}
>
{frames.map((frame) => {
const active = frame.index === f.index
const checked = selected.has(frame.index)
return (
<div
key={frame.index}
className={`overflow-hidden rounded-md border bg-black/35 transition ${
active
? "border-emerald-300/70 shadow-[0_0_0_1px_rgba(110,231,183,0.22)]"
: "border-white/10 hover:border-emerald-300/45"
}`}
>
<button
type="button"
onClick={() => onChange(frame.index)}
className="block w-full text-left"
title={`设为生成目标:分镜 ${frame.index + 1}`}
>
<div className="relative aspect-[9/13] bg-black">
<img
src={referenceFrameSrc(frame)}
alt={`scene reference ${frame.index}`}
className="h-full w-full object-contain"
draggable={false}
/>
<span className="absolute left-1 top-1 rounded bg-black/65 px-1 py-0.5 text-[8.5px] font-mono text-white/80">
{String(frame.index + 1).padStart(2, "0")}
</span>
{active && (
<span className="absolute right-1 top-1 rounded bg-emerald-500/80 px-1 py-0.5 text-[8px] text-white">
</span>
)}
</div>
</button>
<div className="flex items-center justify-between gap-1 px-1.5 py-1 text-[9.5px] text-white/52">
<span>{frame.timestamp.toFixed(2)}s</span>
<button
type="button"
onClick={() => onToggleSelect(frame.index)}
className={`rounded px-1.5 py-0.5 transition ${
checked
? "bg-emerald-400/80 text-black"
: "bg-white/10 text-white/55 hover:bg-white/18 hover:text-white"
}`}
title={checked ? "取消场景参考" : "加入场景参考"}
>
{checked ? "参考" : "选"}
</button>
</div>
</div>
)
})}
</div>
<div className="mt-2 text-[10px] leading-relaxed text-white/38">
</div>
</section>
) : (
<div
ref={imgWrapRef}
@@ -666,7 +759,7 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
{/* 右侧主体识别 + 主体资产 */}
<div
className="flex flex-col gap-2.5 overflow-y-auto min-h-0"
style={isSubjectTab
style={isSubjectTab || isSceneTab
? { flex: "0 0 360px", width: 360, minWidth: 320 }
: { flex: "0 0 320px", width: 320, minWidth: 280, maxWidth: 340 }}
>
@@ -817,12 +910,24 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
<option value="2048">2048</option>
</select>
</div>
<div className="mb-2 rounded-md border border-white/10 bg-black/25 px-2 py-1.5 text-[10px] leading-relaxed text-white/50">
</div>
<div className="mb-2 grid grid-cols-2 gap-1.5">
<label className="space-y-1">
<span className="block text-[9px] text-white/35"></span>
<div className="mb-2 rounded-md border border-white/10 bg-black/25 px-2 py-1.5 text-[10px] leading-relaxed text-white/50">
prompt
</div>
<div className="mb-2 grid grid-cols-2 gap-1.5">
<label className="space-y-1">
<span className="block text-[9px] text-white/35"></span>
<select
value={sceneLocation}
onChange={(e) => setSceneLocation(e.target.value)}
className="w-full rounded border border-white/10 bg-black/35 px-1.5 py-1 text-[10px] text-white/75 outline-none"
>
{SCENE_LOCATION_OPTIONS.map(([value, label]) => (
<option key={value} value={value}>{label}</option>
))}
</select>
</label>
<label className="space-y-1">
<span className="block text-[9px] text-white/35"></span>
<select
value={sceneMode}
onChange={(e) => setSceneMode(e.target.value as SceneMode)}
@@ -830,11 +935,13 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
>
{SCENE_MODE_OPTIONS.map(([value, label]) => (
<option key={value} value={value}>{label}</option>
))}
</select>
</label>
<label className="space-y-1">
<span className="block text-[9px] text-white/35"></span>
))}
</select>
</label>
</div>
<div className="mb-2 grid grid-cols-2 gap-1.5">
<label className="space-y-1">
<span className="block text-[9px] text-white/35"></span>
<select
value={sceneStyle}
onChange={(e) => setSceneStyle(e.target.value as SceneStyle)}
@@ -842,11 +949,63 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
>
{SCENE_STYLE_OPTIONS.map(([value, label]) => (
<option key={value} value={value}>{label}</option>
))}
</select>
</label>
</div>
{!hasSubjectAssets && (
))}
</select>
</label>
<label className="space-y-1">
<span className="block text-[9px] text-white/35"></span>
<input
value={sceneExtraKeywords}
onChange={(e) => setSceneExtraKeywords(e.target.value)}
placeholder="例如:玻璃、金属、夜景"
className="w-full rounded border border-white/10 bg-black/35 px-1.5 py-1 text-[10px] text-white/75 outline-none placeholder:text-white/25"
/>
</label>
</div>
<div className="mb-2 rounded-md border border-white/10 bg-black/20 p-2">
<div className="mb-1 text-[9px] text-white/35"></div>
<div className="flex flex-wrap gap-1">
{SCENE_REFERENCE_OPTIONS.map(([value, label]) => {
const active = sceneReferenceKeys.includes(value)
return (
<button
key={value}
type="button"
onClick={() => setSceneReferenceKeys((prev) => (
prev.includes(value)
? prev.filter((item) => item !== value)
: [...prev, value]
))}
className={`rounded border px-1.5 py-0.5 text-[9.5px] transition ${
active
? "border-emerald-300/60 bg-emerald-500/35 text-white"
: "border-white/10 bg-black/25 text-white/45 hover:text-white"
}`}
>
{label}
</button>
)
})}
</div>
</div>
<label className="mb-2 block">
<div className="mb-1 flex items-center justify-between gap-2">
<span className="text-[9px] text-white/35"> prompt</span>
<button
type="button"
onClick={() => setScenePrompt(scenePromptDraft)}
className="rounded bg-white/10 px-1.5 py-0.5 text-[9.5px] text-white/60 hover:bg-white/18 hover:text-white"
>
</button>
</div>
<textarea
value={scenePrompt || scenePromptDraft}
onChange={(e) => setScenePrompt(e.target.value)}
className="h-28 w-full resize-none rounded-md border border-white/10 bg-black/35 px-2 py-1.5 text-[10px] leading-relaxed text-white/75 outline-none"
/>
</label>
{!hasSubjectAssets && (
<div className="mb-2 rounded border border-amber-300/25 bg-amber-500/10 px-2 py-1.5 text-[10px] leading-snug text-amber-100/85">
</div>