From 0d86b4cff22af5bd22e402f549b8b24fe7d11b71 Mon Sep 17 00:00:00 2001 From: kang Date: Thu, 14 May 2026 06:33:37 +0800 Subject: [PATCH] auto-save 2026-05-14 06:33 (~5) --- .memory/worklog.json | 40 +++---- api/main.py | 65 +++++++--- docs/source-analysis.html | 21 +++- web/components/lightbox.tsx | 231 +++++++++++++++++++++++++++--------- web/lib/api.ts | 10 +- 5 files changed, 271 insertions(+), 96 deletions(-) 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.tsxDAG 节点定义: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;按“原图/清洗、主体资产、场景图、审核”四个页签组织;左侧只放主图/框选画布,右侧承载当前页操作、状态和结果;主体资产页只确认一个统一主体,默认用全部关键帧或已选关键帧作为参考;场景图依赖主体资产,按当前关键帧生成去主体原场景、相似新场景或同构换风格。相关接口包括 cleanupFrameaddElementgenerateSubjectAssetsgenerateSceneAsset
+
主要源码FrameLightbox;按“原图/清洗、主体资产、场景图、审核”四个页签组织;左侧只放主图/框选画布,但主体资产页左侧改为全部参考帧网格;右侧承载当前页操作、状态和结果;主体资产页只确认一个统一主体,默认用全部关键帧或已选关键帧作为参考,后端按参考重绘六张纯背景、占满画面的标准站立主体图;场景图依赖主体资产,按当前关键帧生成去主体原场景、相似新场景或同构换风格。相关接口包括 cleanupFrameaddElementgenerateSubjectAssetsgenerateSceneAsset
适合怎么描述“这一组关键帧如何共同生成一个统一主体包;某张关键帧的水印、去主体场景图和质量风险应该如何审核”。
@@ -730,7 +730,7 @@ SubjectAsset { 应用清洗POST /cleanup/applyapplyCleanedFrame物理覆盖 frames/{idx}.jpg,并备份原图。 元素增改删POST/PATCH/DELETE /elementsaddElement/updateElement/deleteElement让用户修正 Vision 错误,避免候选结果锁死。 元素提取POST /elements/{element_id}/cutoutcutoutElement调用图像模型生成独立白底素材图,每次累积一张 cutout。 - 主体资产包POST /elements/{element_id}/subject-assetsgenerateSubjectAssets根据用户选择的视图、动作和表情生成一个统一主体资产包;前端默认把全部关键帧作为 source_frame_indices,如果用户手动选择了关键帧则只传已选帧,后端拼参考板。 + 主体资产包POST /elements/{element_id}/subject-assetsgenerateSubjectAssets根据参考帧重新绘制一个统一主体资产包;前端默认把全部关键帧作为 source_frame_indices,如果用户手动选择了关键帧则只传已选帧,后端拼参考板。默认输出六张标准站立/转身参考图,纯白/黑背景,不含其他元素,并裁去空白让主体占满画面。 场景资产POST /frames/{idx}/scene-assetgenerateSceneAsset在统一主体资产之后,按当前关键帧生成去主体背景板;请求包含 scene_modescene_style,可做原场景补背景、相似新场景或同构换风格,保留历史版本用于人工审核。 分镜保存PUT /frames/{idx}/storyboardupdateStoryboard保存 4 图槽、时长和改造说明。 生图POST /frames/{idx}/generategenerateImage基于关键帧或已选生成图做 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.tsxweb/components/nodes/index.tsxapi/main.pydocs/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 */} -
- {`frame -
- {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 && ( -
+ 这些参考帧会一起传给模型,用来重绘同一个主体;不是逐张抠图。 +
+ + ) : ( +
+ {`frame - )} - - {/* 画框模式角标(小,左上) — 不再遮挡画面 */} - {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) {