diff --git a/.memory/worklog.json b/.memory/worklog.json
index 71299f2..f26f330 100644
--- a/.memory/worklog.json
+++ b/.memory/worklog.json
@@ -1,26 +1,5 @@
{
"entries": [
- {
- "files_changed": 1,
- "hash": "f3ec026",
- "message": "auto-save 2026-05-12 17:34 (~1)",
- "ts": "2026-05-12T17:34:26+08:00",
- "type": "commit"
- },
- {
- "files_changed": 1,
- "hash": "0a2cfe2",
- "message": "auto-save 2026-05-12 17:39 (~1)",
- "ts": "2026-05-12T17:39:59+08:00",
- "type": "commit"
- },
- {
- "files_changed": 1,
- "hash": "92bd66f",
- "message": "auto-save 2026-05-12 17:45 (~1)",
- "ts": "2026-05-12T17:45:30+08:00",
- "type": "commit"
- },
{
"files_changed": 1,
"hash": "440164e",
@@ -3350,6 +3329,25 @@
"type": "session-heartbeat",
"message": "Claude 会话活跃 · 最近命令:claude · 1 项未提交变更 · 最近提交:auto-save 2026-05-14 06:22 (~2)",
"files_changed": 1
+ },
+ {
+ "ts": "2026-05-14T06:28:04+08:00",
+ "type": "commit",
+ "message": "auto-save 2026-05-14 06:27 (~4)",
+ "hash": "6480d69",
+ "files_changed": 4
+ },
+ {
+ "ts": "2026-05-13T22:28:51Z",
+ "type": "session-heartbeat",
+ "message": "Codex 会话活跃 · 最近命令:codex · 2 项未提交变更 · 最近提交:auto-save 2026-05-14 06:27 (~4)",
+ "files_changed": 2
+ },
+ {
+ "ts": "2026-05-13T22:33:14Z",
+ "type": "session-heartbeat",
+ "message": "Claude 会话活跃 · 最近命令:claude · 5 项未提交变更 · 最近提交:auto-save 2026-05-14 06:27 (~4)",
+ "files_changed": 5
}
]
}
diff --git a/api/main.py b/api/main.py
index 6bfccac..dfffc64 100644
--- a/api/main.py
+++ b/api/main.py
@@ -1957,6 +1957,8 @@ class GenerateSceneAssetReq(BaseModel):
size: AssetSize = "source"
scene_mode: SceneMode = "remove_subject"
scene_style: SceneStyle = "source"
+ prompt: str = ""
+ source_frame_indices: list[int] | None = None
class GenerateSubjectAssetsReq(BaseModel):
@@ -2096,6 +2098,18 @@ def generate_scene_asset(job_id: str, idx: int, req: GenerateSceneAssetReq) -> J
if not src.exists():
raise HTTPException(404, "source frame file missing")
+ source_indices = [int(x) for x in (req.source_frame_indices or [idx]) if isinstance(x, int) or str(x).isdigit()]
+ if not source_indices:
+ source_indices = [idx]
+ source_indices = list(dict.fromkeys(source_indices))[:8]
+ model_src = src
+ sheet_tmp: Path | None = None
+ if len(source_indices) > 1:
+ sheet_tmp = job_dir(job_id) / "tmp" / f"scene_refs_{idx:03d}_{uuid.uuid4().hex[:6]}.jpg"
+ sheet = _make_reference_contact_sheet(job_id, source_indices, sheet_tmp)
+ if sheet:
+ model_src = sheet
+
confirmed_subjects = [
(e.name_en or e.name_zh).strip()
for ref_frame in job.frames
@@ -2136,10 +2150,23 @@ def generate_scene_asset(job_id: str, idx: int, req: GenerateSceneAssetReq) -> J
"warm_lifestyle": "Use a warm lifestyle style: realistic lived-in details, soft natural light, approachable atmosphere.",
"cinematic": "Use a cinematic style: dramatic but natural lighting, richer depth, filmic contrast, not fantasy.",
}[req.scene_style]
+ user_prompt = req.prompt.strip()
+ user_prompt_clause = (
+ "User scene direction: " + user_prompt[:1200] + " "
+ if user_prompt
+ else ""
+ )
+ reference_clause = (
+ f"Use the selected reference frame contact sheet as visual evidence for location, composition, lighting, materials, and atmosphere. Reference frame indices: {', '.join(str(i + 1) for i in source_indices)}. "
+ if len(source_indices) > 1
+ else "Use the provided frame as the primary visual reference. "
+ )
prompt = (
"Create one clean high-definition scene/background reference image from this frame. "
+ subject_clause
+ "Do not include the removed subject, duplicate people, animals, products, text, watermark, platform UI, captions, usernames, hashtags, logos, or overlay graphics. "
+ + reference_clause
+ + user_prompt_clause
+ mode_clause + " "
+ style_clause + " "
+ "Enhance clarity and texture while avoiding over-smoothing, warped geometry, or changing important perspective details. "
@@ -2147,9 +2174,13 @@ def generate_scene_asset(job_id: str, idx: int, req: GenerateSceneAssetReq) -> J
)
models = [IMAGE_MODEL, "gemini-3.1-flash-image-preview", "gemini-2.5-flash-image"]
try:
- img_bytes, _mode = _image_edit_call(src, prompt, models=models, fallback_text=False, max_attempts=3, max_side=1280)
+ img_bytes, _mode = _image_edit_call(model_src, prompt, models=models, fallback_text=False, max_attempts=3, max_side=1280)
except RuntimeError as e:
raise HTTPException(500, f"scene asset failed: {e}")
+ finally:
+ if sheet_tmp and sheet_tmp.exists():
+ try: sheet_tmp.unlink()
+ except OSError: pass
asset_id = f"scene_{idx:03d}_{uuid.uuid4().hex[:8]}"
out_path = job_dir(job_id) / "assets" / f"{asset_id}.jpg"
@@ -2306,23 +2337,27 @@ def generate_subject_assets(job_id: str, idx: int, element_id: str, req: Generat
generated: list[SubjectAsset] = []
try:
for view, view_label in _subject_view_labels(req.subject_kind, req.views):
- if view == "side_walk":
- view_prompt = "side view in a natural walking pose, same identity and proportions"
- elif view.startswith("expression_"):
- emotion = view_label.replace("表情", "")
- view_prompt = f"clear {emotion} facial expression reference, frontal or three-quarter standing pose, preserving the same identity"
- elif view.startswith("action_"):
- view_prompt = f"{view_label} reference pose, same identity and proportions"
+ if req.subject_kind == "living":
+ if view.startswith("expression_"):
+ emotion = view_label.replace("表情", "")
+ view_prompt = f"full-body upright standing character reference with a clear {emotion} facial expression"
+ elif view.startswith("action_") or view == "side_walk":
+ view_prompt = f"full-body upright standing character reference, {view_label}, same identity and proportions"
+ else:
+ view_prompt = f"full-body upright standing character reference, {view_label}"
else:
- view_prompt = f"{view_label} view"
+ view_prompt = f"complete object/product reference, {view_label} view"
prompt = (
- f"Use the reference image(s) to generate a single {view_prompt} of the same {target}. "
+ f"Use the reference image(s) only as visual evidence to redraw the same {target}; do not crop, cut out, paste, or extract pixels from the source. "
+ f"Generate one newly rendered {view_prompt} of the same subject. "
f"The subject is a {kind_phrase}. If multiple frames are shown, treat them as evidence of one same subject, not multiple subjects. "
"Preserve identity, proportions, silhouette, material, colors, styling, and distinctive details across all generated views. "
- f"Create a high-definition standalone asset on a {bg_phrase} background. "
- "No extra objects, no original scene fragments, no text, no watermark, no UI. "
- "If the source is incomplete or occluded, intelligently complete missing parts while staying consistent with the reference. "
- "For living subjects, keep the body standing and readable; do not create medical, horror, or distorted anatomy."
+ "The subject must be complete, centered, full body or full object, head-to-feet visible when applicable, not cropped by the canvas. "
+ "Make the subject large and readable: it should occupy about 85-95% of the image height with only small margins. "
+ f"Create a high-definition standalone asset on a solid {bg_phrase} background. "
+ "No extra objects, no props, no additional products, no background elements, no original scene fragments, no shadows from the original scene, no text, no watermark, no UI. "
+ "If the source is incomplete, partially visible, occluded, or low resolution, reconstruct the missing parts by redrawing a clean complete subject while staying consistent with the reference. "
+ "For living subjects, keep a normal upright standing pose for the standard views; do not create sitting, walking, medical, horror, or distorted anatomy unless explicitly requested by the view label."
)
try:
img_bytes, _mode = _image_edit_call(model_src, prompt, models=models, fallback_text=False, max_attempts=3, max_side=1280)
@@ -2331,7 +2366,7 @@ def generate_subject_assets(job_id: str, idx: int, element_id: str, req: Generat
asset_id = f"subject_{idx:03d}_{element_id}_{view}_{uuid.uuid4().hex[:8]}"
out_path = job_dir(job_id) / "assets" / f"{asset_id}.jpg"
- width, height = _normalize_asset_image(img_bytes, out_path, _source_frame_path(job_id, idx), req.size, req.background, square=False)
+ width, height = _normalize_asset_image(img_bytes, out_path, _source_frame_path(job_id, idx), req.size, req.background, square=False, fill_subject=True)
generated.append(SubjectAsset(
id=asset_id,
view=view,
diff --git a/docs/source-analysis.html b/docs/source-analysis.html
index 52ae5d3..d043fc7 100644
--- a/docs/source-analysis.html
+++ b/docs/source-analysis.html
@@ -555,7 +555,7 @@
2
镜头拆解
拆轨、抽关键帧、手动加帧,形成参考分镜池。
3
清洗水印
对关键帧做全图或区域清洗,必要时应用为当前参考图。
4
主体识别
识别场景和主体候选,只是候选,不应锁死。
- 5
素材准备
清洗关键帧,把多张关键帧作为同一主体的参考,先生成一个统一主体资产包,再按关键帧生成多个去主体、相似或换风格场景图。
+ 5
素材准备
清洗关键帧,把多张关键帧作为同一主体的参考,先重绘六张标准站立主体资产图,再按关键帧生成多个去主体、相似或换风格场景图。
6
分镜改造
把参考主体、场景、动作和 SKG 产品放入分镜结构。
7
生成视频
用分镜 4 图槽、改造目标和时长调用 Seedance / Kling / Veo 3 生视频 API,结果回写到画面工作台节点。
8
合成成品
片段、字幕、配音、转场合成最终 mp4。当前未实现。
@@ -571,7 +571,7 @@
web/app/page.tsx | 产品工作台主状态:jobs、activeJobId、selectedFrames、clipboard、ReactFlow 节点和边;负责打开/找回画布工作面板。 |
web/components/nodes/index.tsx | DAG 节点定义:Input、VisualLab、Audio、Compose,以及画布工作面板 KeyframePanel / VideoFramePanel;旧 Keyframe/Storyboard/VideoGen 组件保留但不再挂主画布。 |
- web/components/lightbox.tsx | 关键帧素材准备面板:清洗、统一主体候选、统一主体资产包、每帧去主体场景图和审核。 |
+ web/components/lightbox.tsx | 关键帧素材准备面板:清洗、统一主体候选、参考帧网格、六张主体重绘图、每帧去主体场景图和审核。 |
web/components/storyboard-bar.tsx | 顶部分镜编排条:展示选入编排的关键帧,并作为唯一分镜导航。 |
web/components/storyboard-workbench.tsx | 顶部分镜编排条下方的明细区:4 图槽、改造目标、时长、自动保存。 |
web/lib/api.ts | 前端类型和 API client,是前后端数据契约镜像。 |
@@ -623,7 +623,7 @@ api/main.py
你看到的区域关键帧素材审核面板
-
主要源码FrameLightbox;按“原图/清洗、主体资产、场景图、审核”四个页签组织;左侧只放主图/框选画布,右侧承载当前页操作、状态和结果;主体资产页只确认一个统一主体,默认用全部关键帧或已选关键帧作为参考;场景图依赖主体资产,按当前关键帧生成去主体原场景、相似新场景或同构换风格。相关接口包括 cleanupFrame、addElement、generateSubjectAssets、generateSceneAsset。
+
主要源码FrameLightbox;按“原图/清洗、主体资产、场景图、审核”四个页签组织;左侧只放主图/框选画布,但主体资产页左侧改为全部参考帧网格;右侧承载当前页操作、状态和结果;主体资产页只确认一个统一主体,默认用全部关键帧或已选关键帧作为参考,后端按参考重绘六张纯背景、占满画面的标准站立主体图;场景图依赖主体资产,按当前关键帧生成去主体原场景、相似新场景或同构换风格。相关接口包括 cleanupFrame、addElement、generateSubjectAssets、generateSceneAsset。
适合怎么描述“这一组关键帧如何共同生成一个统一主体包;某张关键帧的水印、去主体场景图和质量风险应该如何审核”。
@@ -730,7 +730,7 @@ SubjectAsset {
| 应用清洗 | POST /cleanup/apply | applyCleanedFrame | 物理覆盖 frames/{idx}.jpg,并备份原图。 |
| 元素增改删 | POST/PATCH/DELETE /elements | addElement/updateElement/deleteElement | 让用户修正 Vision 错误,避免候选结果锁死。 |
| 元素提取 | POST /elements/{element_id}/cutout | cutoutElement | 调用图像模型生成独立白底素材图,每次累积一张 cutout。 |
-
| 主体资产包 | POST /elements/{element_id}/subject-assets | generateSubjectAssets | 根据用户选择的视图、动作和表情生成一个统一主体资产包;前端默认把全部关键帧作为 source_frame_indices,如果用户手动选择了关键帧则只传已选帧,后端拼参考板。 |
+
| 主体资产包 | POST /elements/{element_id}/subject-assets | generateSubjectAssets | 根据参考帧重新绘制一个统一主体资产包;前端默认把全部关键帧作为 source_frame_indices,如果用户手动选择了关键帧则只传已选帧,后端拼参考板。默认输出六张标准站立/转身参考图,纯白/黑背景,不含其他元素,并裁去空白让主体占满画面。 |
| 场景资产 | POST /frames/{idx}/scene-asset | generateSceneAsset | 在统一主体资产之后,按当前关键帧生成去主体背景板;请求包含 scene_mode 和 scene_style,可做原场景补背景、相似新场景或同构换风格,保留历史版本用于人工审核。 |
| 分镜保存 | PUT /frames/{idx}/storyboard | updateStoryboard | 保存 4 图槽、时长和改造说明。 |
| 生图 | POST /frames/{idx}/generate | generateImage | 基于关键帧或已选生成图做 image-to-image,目前可用。 |
@@ -841,6 +841,19 @@ SubjectAsset {
变更记录
这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。
+
+
+ 2026-05-14 · 主体资产改为参考重绘六张标准图
+ FrameLightbox
+ Assets
+
+
+
问题:主体资产不是抠图,也不是只看当前单帧生成多角度;主体页需要看到全部参考帧,并用这些参考重新绘制一个完整主体。
+
改动:FrameLightbox 在“主体资产”页左侧显示全部参考帧网格,小图排列,可点击切换当前帧;右侧仍负责统一主体确认和生成。人物/生物默认视图改为六张标准站立/转身图:正面、背面、左侧、右侧、左前 45°、右前 45°。
+
后端:generateSubjectAssets prompt 改为“参考重绘”,明确禁止裁剪/抠图/粘贴源像素,要求主体完整居中、纯白/黑背景、无其他元素,并占画面约 85-95% 高度;落盘时会裁掉纯背景空白并放大主体。
+
影响:web/components/lightbox.tsx、web/components/nodes/index.tsx、api/main.py、docs/source-analysis.html。
+
+
2026-05-14 · 主体资产改为统一主体参考帧生成
diff --git a/web/components/lightbox.tsx b/web/components/lightbox.tsx
index cfe44cd..8406f07 100644
--- a/web/components/lightbox.tsx
+++ b/web/components/lightbox.tsx
@@ -65,6 +65,25 @@ const SCENE_STYLE_OPTIONS: Array<[SceneStyle, string]> = [
["cinematic", "电影感"],
]
+const SCENE_LOCATION_OPTIONS = [
+ ["modern living room", "现代客厅"],
+ ["minimal studio", "极简影棚"],
+ ["premium bathroom", "高端浴室"],
+ ["bedroom nightstand", "卧室床头"],
+ ["office desk", "办公桌面"],
+ ["retail display", "零售陈列"],
+ ["outdoor patio", "户外露台"],
+]
+
+const SCENE_REFERENCE_OPTIONS = [
+ ["camera angle and composition", "构图/机位"],
+ ["lighting direction", "光线方向"],
+ ["material textures", "材质纹理"],
+ ["color palette", "色彩氛围"],
+ ["spatial layout", "空间层次"],
+ ["social media realism", "真实生活感"],
+]
+
export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, onChange, onToggleSelect, onJobUpdate, onSwitchPanel, onCopyImage, embedded = false }: Props) {
const [describing, setDescribing] = useState(false)
const [cleaningFrameIds, setCleaningFrameIds] = useState>(new Set())
@@ -76,6 +95,10 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
const [assetSize, setAssetSize] = useState("source")
const [sceneMode, setSceneMode] = useState("remove_subject")
const [sceneStyle, setSceneStyle] = useState("source")
+ const [sceneLocation, setSceneLocation] = useState("modern living room")
+ const [sceneReferenceKeys, setSceneReferenceKeys] = useState(["camera angle and composition", "lighting direction", "spatial layout"])
+ const [sceneExtraKeywords, setSceneExtraKeywords] = useState("")
+ const [scenePrompt, setScenePrompt] = useState("")
const [subjectKinds, setSubjectKinds] = useState>({})
const [subjectBackgrounds, setSubjectBackgrounds] = useState>({})
const [subjectViews, setSubjectViews] = useState>({})
@@ -134,11 +157,23 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
const cleanedFrameCount = frames.filter((frame) => frame.cleaned_applied || frame.cleaned_url).length
const pendingCleanFrames = frames.filter((frame) => !frame.cleaned_applied && !frame.cleaned_url)
const selectedFrameIndices = Array.from(selected).sort((a, b) => a - b)
- const subjectReferenceFrameIndices = (selectedFrameIndices.length > 0 ? selectedFrameIndices : frames.map((frame) => frame.index))
+ const cleanedFrameIndices = frames
+ .filter((frame) => frame.cleaned_applied || frame.cleaned_url)
+ .map((frame) => frame.index)
+ const subjectReferenceFrameIndices = (
+ cleanedFrameIndices.length > 0 || selectedFrameIndices.length > 0
+ ? [...cleanedFrameIndices, ...selectedFrameIndices]
+ : frames.map((frame) => frame.index)
+ )
.filter((idx, pos, arr) => arr.indexOf(idx) === pos)
+ const subjectReferenceFrames = subjectReferenceFrameIndices
+ .map((idx) => frames.find((frame) => frame.index === idx))
+ .filter((frame): frame is KeyFrame => Boolean(frame))
const subjectReferenceLabel = selectedFrameIndices.length > 0
- ? `${subjectReferenceFrameIndices.length} 已选帧参考`
- : `${subjectReferenceFrameIndices.length} 全部帧参考`
+ ? `${subjectReferenceFrameIndices.length} 清洗/已选帧参考`
+ : cleanedFrameIndices.length > 0
+ ? `${subjectReferenceFrameIndices.length} 已清洗帧参考`
+ : `${subjectReferenceFrameIndices.length} 全部帧参考`
const subjectElementRefs = frames.flatMap((frame) =>
(frame.elements ?? []).map((element) => ({
frameIndex: frame.index,
@@ -162,7 +197,29 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
...(latestSceneAsset?.quality_report?.warnings ?? []),
]
const isSubjectTab = activeTab === "subject"
+ const isSceneTab = activeTab === "scene"
const isCleanTab = activeTab === "clean"
+ const sceneReferenceFrameIndices = (selectedFrameIndices.length > 0 ? selectedFrameIndices : [f.index])
+ .filter((idx, pos, arr) => arr.indexOf(idx) === pos)
+ const sceneReferenceFrames = sceneReferenceFrameIndices
+ .map((idx) => frames.find((frame) => frame.index === idx))
+ .filter((frame): frame is KeyFrame => Boolean(frame))
+ const unifiedSubjectName = subjectElementRefs[0]?.element.name_zh || "统一主体"
+ const sceneLocationLabel = SCENE_LOCATION_OPTIONS.find(([value]) => value === sceneLocation)?.[1] ?? sceneLocation
+ const sceneStyleLabel = SCENE_STYLE_OPTIONS.find(([value]) => value === sceneStyle)?.[1] ?? sceneStyle
+ const sceneModeLabel = SCENE_MODE_OPTIONS.find(([value]) => value === sceneMode)?.[1] ?? sceneMode
+ const sceneReferenceLabels = sceneReferenceKeys
+ .map((key) => SCENE_REFERENCE_OPTIONS.find(([value]) => value === key)?.[1] ?? key)
+ const scenePromptDraft = [
+ `主体:移除 ${unifiedSubjectName} 后生成空场景。`,
+ `地点:${sceneLocationLabel}。`,
+ `生成方式:${sceneModeLabel}。`,
+ `风格:${sceneStyleLabel}。`,
+ `参考帧:${sceneReferenceFrames.map((frame) => `分镜${frame.index + 1}`).join("、") || `分镜${f.index + 1}`}。`,
+ sceneReferenceLabels.length > 0 ? `保留参考:${sceneReferenceLabels.join("、")}。` : "",
+ sceneExtraKeywords.trim() ? `额外关键词:${sceneExtraKeywords.trim()}。` : "",
+ "要求:无主体、无人物动物产品、无文字水印,保持可用于后续视频生成的干净背景板。",
+ ].filter(Boolean).join("\n")
const handleDescribe = async () => {
setDescribing(true)
@@ -237,7 +294,13 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
}
setSceneGenerating(true)
try {
- const updated = await generateSceneAsset(jobId, f.index, { size: assetSize, scene_mode: sceneMode, scene_style: sceneStyle })
+ const updated = await generateSceneAsset(jobId, f.index, {
+ size: assetSize,
+ scene_mode: sceneMode,
+ scene_style: sceneStyle,
+ prompt: scenePrompt.trim() || scenePromptDraft,
+ source_frame_indices: sceneReferenceFrameIndices,
+ })
onJobUpdate?.(updated)
toast.success(`分镜 ${f.index + 1} 场景图已生成`)
} catch (e) {
@@ -388,6 +451,13 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
})()
// bust cache:替换后 frames/{idx}.jpg 内容已变,要刷新
const mainSrc = `${frameUrl(jobId, f.index)}${f.cleaned_applied ? "?applied=1" : ""}`
+ const referenceFrameSrc = (frame: KeyFrame) => {
+ if (frame.cleaned_url) {
+ const ts = frame.cleaned_url.match(/t=(\d+)/)?.[1]
+ return cleanedFrameUrl(jobId, frame.index, ts)
+ }
+ return `${frameUrl(jobId, frame.index)}${frame.cleaned_applied ? "?applied=1" : ""}`
+ }
const content = (
- {/* 上方:主图 + 画框 overlay */}
-
-

-
- {f.cleaned_applied ? "✨ 已替换为清洗版" : "原图"}
-
-
- {/* 已确认的多个选区 */}
- {cropMode && regions.map((r, i) => (
-
+
+
主体参考帧
+
{subjectReferenceLabel}
+
+
-
#{i + 1}
+ {subjectReferenceFrames.map((frame) => {
+ const active = frame.index === f.index
+ return (
+
+ )
+ })}
- ))}
-
- {/* 当前正在拖的草稿框 */}
- {cropMode && draftRegion && draftRegion.w > 0 && draftRegion.h > 0 && (
-
+ 这些参考帧会一起传给模型,用来重绘同一个主体;不是逐张抠图。
+
+
+ ) : (
+
+

- )}
-
- {/* 画框模式角标(小,左上) — 不再遮挡画面 */}
- {cropMode && (
-
- 画框 · 已选 {regions.length}
+
+ {f.cleaned_applied ? "✨ 已替换为清洗版" : "原图"}
- )}
-
+
+ {/* 已确认的多个选区 */}
+ {cropMode && regions.map((r, i) => (
+
+ #{i + 1}
+
+ ))}
+
+ {/* 当前正在拖的草稿框 */}
+ {cropMode && draftRegion && draftRegion.w > 0 && draftRegion.h > 0 && (
+
+ )}
+
+ {/* 画框模式角标(小,左上) — 不再遮挡画面 */}
+ {cropMode && (
+
+ 画框 · 已选 {regions.length}
+
+ )}
+
+ )}
diff --git a/web/lib/api.ts b/web/lib/api.ts
index 1de94e6..f8ba96a 100644
--- a/web/lib/api.ts
+++ b/web/lib/api.ts
@@ -657,7 +657,13 @@ export async function cutoutElement(jobId: string, frameIdx: number, elementId:
export async function generateSceneAsset(
jobId: string,
frameIdx: number,
- body: { size?: AssetSize; scene_mode?: SceneMode; scene_style?: SceneStyle } = {},
+ body: {
+ size?: AssetSize
+ scene_mode?: SceneMode
+ scene_style?: SceneStyle
+ prompt?: string
+ source_frame_indices?: number[]
+ } = {},
): Promise
{
const res = await fetch(`${API_BASE}/jobs/${jobId}/frames/${frameIdx}/scene-asset`, {
method: "POST",
@@ -667,6 +673,8 @@ export async function generateSceneAsset(
size: body.size ?? "source",
scene_mode: body.scene_mode ?? "remove_subject",
scene_style: body.scene_style ?? "source",
+ prompt: body.prompt ?? "",
+ source_frame_indices: body.source_frame_indices ?? null,
}),
})
if (!res.ok) {