auto-save 2026-05-18 06:33 (~5)
This commit is contained in:
@@ -1,51 +1,5 @@
|
|||||||
{
|
{
|
||||||
"entries": [
|
"entries": [
|
||||||
{
|
|
||||||
"files_changed": 1,
|
|
||||||
"message": "Codex 会话活跃 · 最近命令:codex · 1 项未提交变更 · 最近提交:auto-save 2026-05-15 15:59 (~4)",
|
|
||||||
"ts": "2026-05-15T08:04:47Z",
|
|
||||||
"type": "session-heartbeat"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"files_changed": 1,
|
|
||||||
"hash": "ac36c4e",
|
|
||||||
"message": "auto-save 2026-05-15 16:05 (~1)",
|
|
||||||
"ts": "2026-05-15T16:05:39+08:00",
|
|
||||||
"type": "commit"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"files_changed": 5,
|
|
||||||
"hash": "5ca9846",
|
|
||||||
"message": "auto-save 2026-05-15 16:10 (+1, ~4)",
|
|
||||||
"ts": "2026-05-15T16:11:10+08:00",
|
|
||||||
"type": "commit"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"files_changed": 1,
|
|
||||||
"message": "Codex 会话活跃 · 最近命令:codex · 1 项未提交变更 · 最近提交:auto-save 2026-05-15 16:10 (+1, ~4)",
|
|
||||||
"ts": "2026-05-15T08:14:47Z",
|
|
||||||
"type": "session-heartbeat"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"files_changed": 1,
|
|
||||||
"hash": "e262285",
|
|
||||||
"message": "auto-save 2026-05-15 16:16 (~1)",
|
|
||||||
"ts": "2026-05-15T16:16:43+08:00",
|
|
||||||
"type": "commit"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"files_changed": 1,
|
|
||||||
"hash": "36eb205",
|
|
||||||
"message": "auto-save 2026-05-15 16:22 (~1)",
|
|
||||||
"ts": "2026-05-15T16:22:15+08:00",
|
|
||||||
"type": "commit"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"files_changed": 1,
|
|
||||||
"message": "Codex 会话活跃 · 最近命令:codex · 1 项未提交变更 · 最近提交:auto-save 2026-05-15 16:22 (~1)",
|
|
||||||
"ts": "2026-05-15T08:24:47Z",
|
|
||||||
"type": "session-heartbeat"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"files_changed": 3,
|
"files_changed": 3,
|
||||||
"hash": "ecc5894",
|
"hash": "ecc5894",
|
||||||
@@ -3257,6 +3211,50 @@
|
|||||||
"message": "auto-save 2026-05-18 01:02 (~2)",
|
"message": "auto-save 2026-05-18 01:02 (~2)",
|
||||||
"hash": "4c43d89",
|
"hash": "4c43d89",
|
||||||
"files_changed": 2
|
"files_changed": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-05-18T01:07:51+08:00",
|
||||||
|
"type": "commit",
|
||||||
|
"message": "auto-save 2026-05-18 01:07 (~8)",
|
||||||
|
"hash": "7ca5a95",
|
||||||
|
"files_changed": 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-05-17T17:08:32Z",
|
||||||
|
"type": "session-heartbeat",
|
||||||
|
"message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:auto-save 2026-05-18 01:07 (~8)",
|
||||||
|
"files_changed": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-05-17T17:18:32Z",
|
||||||
|
"type": "session-heartbeat",
|
||||||
|
"message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:auto-save 2026-05-18 01:07 (~8)",
|
||||||
|
"files_changed": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-05-17T17:28:32Z",
|
||||||
|
"type": "session-heartbeat",
|
||||||
|
"message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:auto-save 2026-05-18 01:07 (~8)",
|
||||||
|
"files_changed": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-05-17T19:28:08Z",
|
||||||
|
"type": "session-heartbeat",
|
||||||
|
"message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:auto-save 2026-05-18 01:07 (~8)",
|
||||||
|
"files_changed": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-05-18T06:22:31+08:00",
|
||||||
|
"type": "commit",
|
||||||
|
"message": "fix: force gpt image model",
|
||||||
|
"hash": "4a5c549",
|
||||||
|
"files_changed": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-05-17T22:23:44Z",
|
||||||
|
"type": "session-heartbeat",
|
||||||
|
"message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:fix: force gpt image model",
|
||||||
|
"files_changed": 1
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
59
api/main.py
59
api/main.py
@@ -1134,6 +1134,17 @@ def _asset_url(job_id: str, asset_id: str) -> str:
|
|||||||
return f"/jobs/{job_id}/assets/{asset_id}.jpg"
|
return f"/jobs/{job_id}/assets/{asset_id}.jpg"
|
||||||
|
|
||||||
|
|
||||||
|
def _delete_subject_asset_file(job_id: str, asset_id: str) -> None:
|
||||||
|
if not asset_id:
|
||||||
|
return
|
||||||
|
p = job_dir(job_id) / "assets" / f"{asset_id}.jpg"
|
||||||
|
if p.exists():
|
||||||
|
try:
|
||||||
|
p.unlink()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _find_frame(job: Job, idx: int) -> KeyFrame:
|
def _find_frame(job: Job, idx: int) -> KeyFrame:
|
||||||
frame = next((f for f in job.frames if f.index == idx), None)
|
frame = next((f for f in job.frames if f.index == idx), None)
|
||||||
if not frame:
|
if not frame:
|
||||||
@@ -3560,6 +3571,7 @@ class GenerateSubjectAssetsReq(BaseModel):
|
|||||||
subject_style: Literal["transparent_human", "source_actor"] = "transparent_human"
|
subject_style: Literal["transparent_human", "source_actor"] = "transparent_human"
|
||||||
reconstruction_mode: Literal["same", "similar"] = "same"
|
reconstruction_mode: Literal["same", "similar"] = "same"
|
||||||
prompt: str = ""
|
prompt: str = ""
|
||||||
|
replace_views: bool = False
|
||||||
|
|
||||||
|
|
||||||
class UpdateProductRefsReq(BaseModel):
|
class UpdateProductRefsReq(BaseModel):
|
||||||
@@ -3974,6 +3986,17 @@ def generate_subject_assets(job_id: str, idx: int, element_id: str, req: Generat
|
|||||||
if sheet:
|
if sheet:
|
||||||
model_src = sheet
|
model_src = sheet
|
||||||
|
|
||||||
|
try:
|
||||||
|
with Image.open(_source_frame_path(job_id, idx)) as src_im:
|
||||||
|
source_is_portrait = src_im.height > src_im.width
|
||||||
|
except Exception:
|
||||||
|
source_is_portrait = False
|
||||||
|
canvas_clause = (
|
||||||
|
"Canvas and aspect ratio: the reference video frame is vertical, so output a vertical portrait 9:16-style image, not a square canvas and not a horizontal layout. "
|
||||||
|
if source_is_portrait
|
||||||
|
else "Canvas and aspect ratio: keep a single clean reference-image canvas with the same broad orientation as the source evidence. "
|
||||||
|
)
|
||||||
|
|
||||||
target = (el.name_en or el.name_zh).strip()
|
target = (el.name_en or el.name_zh).strip()
|
||||||
bg_phrase = "pure white" if req.background == "white" else "pure black"
|
bg_phrase = "pure white" if req.background == "white" else "pure black"
|
||||||
similar_actor = req.subject_kind == "living" and req.subject_style == "source_actor" and req.reconstruction_mode == "similar"
|
similar_actor = req.subject_kind == "living" and req.subject_style == "source_actor" and req.reconstruction_mode == "similar"
|
||||||
@@ -4036,6 +4059,7 @@ def generate_subject_assets(job_id: str, idx: int, element_id: str, req: Generat
|
|||||||
+ single_view_clause
|
+ single_view_clause
|
||||||
+ identity_clause
|
+ identity_clause
|
||||||
+ identity_lock_clause
|
+ identity_lock_clause
|
||||||
|
+ canvas_clause
|
||||||
+ prompt_extra_clause
|
+ prompt_extra_clause
|
||||||
+ actor_style_clause
|
+ actor_style_clause
|
||||||
+ "The subject must be complete, centered, full body or full object, head-to-feet visible when applicable, not cropped by the canvas. "
|
+ "The subject must be complete, centered, full body or full object, head-to-feet visible when applicable, not cropped by the canvas. "
|
||||||
@@ -4082,12 +4106,45 @@ def generate_subject_assets(job_id: str, idx: int, element_id: str, req: Generat
|
|||||||
if e.id == element_id:
|
if e.id == element_id:
|
||||||
e.subject_kind = req.subject_kind
|
e.subject_kind = req.subject_kind
|
||||||
e.cutout_background = req.background
|
e.cutout_background = req.background
|
||||||
e.subject_assets = (e.subject_assets or []) + generated
|
current_assets = e.subject_assets or []
|
||||||
|
if req.replace_views:
|
||||||
|
replaced_views = {asset.view for asset in generated}
|
||||||
|
for old_asset in current_assets:
|
||||||
|
if old_asset.view in replaced_views:
|
||||||
|
_delete_subject_asset_file(job_id, old_asset.id)
|
||||||
|
current_assets = [asset for asset in current_assets if asset.view not in replaced_views]
|
||||||
|
e.subject_assets = current_assets + generated
|
||||||
new_frames.append(f)
|
new_frames.append(f)
|
||||||
update(job, frames=new_frames, message=f"主体资产包生成完成 · {el.name_zh} · {len(generated)} 张")
|
update(job, frames=new_frames, message=f"主体资产包生成完成 · {el.name_zh} · {len(generated)} 张")
|
||||||
return job
|
return job
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/jobs/{job_id}/frames/{idx}/elements/{element_id}/subject-assets/{asset_id}", response_model=Job)
|
||||||
|
def delete_subject_asset(job_id: str, idx: int, element_id: str, asset_id: str) -> Job:
|
||||||
|
"""删除某张主体白底视图。"""
|
||||||
|
job = JOBS.get(job_id)
|
||||||
|
if not job:
|
||||||
|
raise HTTPException(404, "job not found")
|
||||||
|
frame = _find_frame(job, idx)
|
||||||
|
el = next((e for e in frame.elements if e.id == element_id), None)
|
||||||
|
if not el:
|
||||||
|
raise HTTPException(404, "element not found")
|
||||||
|
assets = el.subject_assets or []
|
||||||
|
if not any(asset.id == asset_id for asset in assets):
|
||||||
|
raise HTTPException(404, "subject asset not found")
|
||||||
|
|
||||||
|
_delete_subject_asset_file(job_id, asset_id)
|
||||||
|
new_frames = []
|
||||||
|
for f in job.frames:
|
||||||
|
if f.index == idx:
|
||||||
|
for e in f.elements:
|
||||||
|
if e.id == element_id:
|
||||||
|
e.subject_assets = [asset for asset in (e.subject_assets or []) if asset.id != asset_id]
|
||||||
|
new_frames.append(f)
|
||||||
|
update(job, frames=new_frames, message=f"主体视图已删除 · {el.name_zh}")
|
||||||
|
return job
|
||||||
|
|
||||||
|
|
||||||
@app.delete("/jobs/{job_id}/frames/{idx}/elements/{element_id}/cutouts/{cutout_id}", response_model=Job)
|
@app.delete("/jobs/{job_id}/frames/{idx}/elements/{element_id}/cutouts/{cutout_id}", response_model=Job)
|
||||||
def delete_cutout(job_id: str, idx: int, element_id: str, cutout_id: str) -> Job:
|
def delete_cutout(job_id: str, idx: int, element_id: str, cutout_id: str) -> Job:
|
||||||
"""删除该元素的某张提取图"""
|
"""删除该元素的某张提取图"""
|
||||||
|
|||||||
@@ -589,7 +589,7 @@
|
|||||||
<tr><td><code>web/next.config.mjs</code></td><td>Next.js 构建配置:静态导出、图片不走优化、禁用开发环境左下角 Next Dev Indicator,并移除 Next 16 已不支持的 <code>eslint</code> 顶层配置,避免本地 dev 出现配置 Issue 提示。</td></tr>
|
<tr><td><code>web/next.config.mjs</code></td><td>Next.js 构建配置:静态导出、图片不走优化、禁用开发环境左下角 Next Dev Indicator,并移除 Next 16 已不支持的 <code>eslint</code> 顶层配置,避免本地 dev 出现配置 Issue 提示。</td></tr>
|
||||||
<tr><td><code>web/app/globals.css</code></td><td>全局主题变量、登录页视觉样式、ReactFlow 样式引用,以及本地开发态 <code>nextjs-portal</code> 遮挡隐藏规则。</td></tr>
|
<tr><td><code>web/app/globals.css</code></td><td>全局主题变量、登录页视觉样式、ReactFlow 样式引用,以及本地开发态 <code>nextjs-portal</code> 遮挡隐藏规则。</td></tr>
|
||||||
<tr><td><code>web/app/page.tsx</code></td><td>产品工作台主状态:jobs、activeJobId、生成任务状态;主渲染为全屏素材输入列 + 信息流广告复刻工作表;“开始”编排状态只负责在下载完成后自动触发 <code>triggerTranscribe</code>,不再默认触发抽帧、Vision 扫描或分镜初稿保存;底部吸附音频条不再从主界面渲染。</td></tr>
|
<tr><td><code>web/app/page.tsx</code></td><td>产品工作台主状态:jobs、activeJobId、生成任务状态;主渲染为全屏素材输入列 + 信息流广告复刻工作表;“开始”编排状态只负责在下载完成后自动触发 <code>triggerTranscribe</code>,不再默认触发抽帧、Vision 扫描或分镜初稿保存;底部吸附音频条不再从主界面渲染。</td></tr>
|
||||||
<tr><td><code>web/components/ad-recreation-board.tsx</code></td><td>信息流广告复刻工作表:左侧素材输入只负责链接/上传和任务切换,不再重复放横版原视频预览;右侧展示视频下载状态、默认折叠的文案依据,以及源视频工作区。音频解析结果改成默认折叠的辅助信息,展开后同一行看讲话人/节奏/背景音;主工作区左侧是按 9:16 显示的竖版原视频播放器,播放器内覆盖“当前点抽帧”,按当前播放秒数手动补参考帧;右侧上方是音频波形 / 切点参考,下方是逐句时间轴;下一行铺开“关键帧 / 相似主体”。音频波形用参考图式的连续灰色包络显示响度、停顿和密集爆点,顶部同时显示当前播放秒数、总时长和鼠标指针停点秒数。视频播放时通过 <code>requestAnimationFrame</code> 平滑驱动波形播放线,同时同步高亮并滚动当前句;点击音频波形或字幕行会跳转原视频时间。关键帧区的主入口是“自动抽帧 12 张”,一键按动作峰值目标重新抽取 12 张源视频参考帧,优先抓手势、表情变化、节奏点和镜头变化,缩略图按竖版完整比例显示不裁切并用更多列紧凑铺开,鼠标停留会通过固定浮层放大展示完整帧。“生成 6 视图”放在相似主体白底视图区,不和抽参考按钮平齐;如果用户没有勾选帧,默认把全部关键帧作为主体参考,勾选后只传已选帧;生成区可在“透明骨架 / 普通真人”之间切换,并可填写统一主体方向,例如年轻女性、更运动、更高级。前端调用 <code>generateSubjectAssets</code> 时按主体类型传 <code>subject_style=transparent_human</code> 或 <code>source_actor</code>,均使用 <code>reconstruction_mode=similar</code>;后端会把这些帧视为同一个主体的证据,并锁定同一性别表现、年龄段、体型、材质、风格和视觉身份,避免六视图出现男女性别、老少年龄或样式混杂。音频结果下方是信息流复刻分镜工作台:顶部产品参考区是“同一产品素材池”,不限量上传产品图,不做不同产品身份判断;上传原图推荐长边 1200-2000px、短边至少 600px,但后端会统一生成最长边 1600px、JPEG 92 的 AI 工作副本,并回显尺寸、自动转换和风险标注;上传后按“套在脖子上的 U 形肩颈按摩仪”进行同一产品批量识别,左/右按佩戴者身体左右、上/下按佩戴方向,额外标注内外侧、开口方向、局部结构点、背景类型、用途标签、生成风险和备注,用户只检查备注,鼠标悬停通过固定浮层显示大图预览,能盖过滚动容器和分镜框架;缺视角补图失败时保留重试入口。脚本区在分镜行上方提供“作者想法”和“整片改写”,每行新口播文案可直接编辑并可单段 AI 改写,分镜时间和原内容列压缩为窄摘要列,把横向空间留给新口播、画面规划和视频候选;生成本条视频时使用当前编辑后的新口播文案。每条音频分镜纵向排列,行内从左到右串起原内容、新口播文案、画面规划/产品融入和 6 个候选视频槽;候选视频槽在宽屏下一排显示 6 个竖版预览,避免前面空旷、后面拥挤。单条生成会从全局选中关键帧或 12 张关键帧中取最贴近本句时间点的参考帧。单条生成会从产品素材池按分镜角色、视角优先级、用途标签、置信度和风险自动挑选最多 6 张相关产品图,不会把全部产品图提交给生视频模型,然后把产品坐标系、视角标注、方向、结构点和风险写入 Seedance 提示。<code>ModelTrace</code> 会在音频解析、产品识别/补图、相似主体 6 视图、脚本改写和单条生视频入口旁直接展示模型名;所有生图入口都显示并使用 <code>gpt-image-2</code>,没有其他图片模型 fallback;点击后用固定浮层展示模型链路、输入输出和回退逻辑。旧分镜卡、抽帧控制和视频生成组件仍保留在文件里,但当前主路径不渲染。</td></tr>
|
<tr><td><code>web/components/ad-recreation-board.tsx</code></td><td>信息流广告复刻工作表:左侧素材输入只负责链接/上传和任务切换,不再重复放横版原视频预览;右侧展示视频下载状态、默认折叠的文案依据,以及源视频工作区。音频解析结果改成默认折叠的辅助信息,展开后同一行看讲话人/节奏/背景音;主工作区左侧是按 9:16 显示的竖版原视频播放器,播放器内覆盖“当前点抽帧”,按当前播放秒数手动补参考帧;右侧上方是音频波形 / 切点参考,下方是逐句时间轴;下一行铺开“关键帧 / 相似主体”。音频波形用参考图式的连续灰色包络显示响度、停顿和密集爆点,顶部同时显示当前播放秒数、总时长和鼠标指针停点秒数。视频播放时通过 <code>requestAnimationFrame</code> 平滑驱动波形播放线,同时同步高亮并滚动当前句;点击音频波形或字幕行会跳转原视频时间。关键帧区的主入口是“自动抽帧 12 张”,一键按动作峰值目标重新抽取 12 张源视频参考帧,优先抓手势、表情变化、节奏点和镜头变化,缩略图按竖版完整比例显示不裁切并用更多列紧凑铺开,鼠标停留会通过固定浮层放大展示完整帧。“生成 6 视图”放在相似主体白底视图区,不和抽参考按钮平齐;如果用户没有勾选帧,默认把全部关键帧作为主体参考,勾选后只传已选帧;生成区可在“透明骨架 / 普通真人”之间切换,并可填写统一主体方向,例如年轻女性、更运动、更高级。关键帧和相似主体白底视图都用更小的竖版缩略图密排;白底视图只展示每个 view 的最新一张,缩略图上提供“重新生成这一张”和“删除这一张”,单张重生会用 <code>replace_views=true</code> 替换同一视角,不追加成第 7 张。前端调用 <code>generateSubjectAssets</code> 时按主体类型传 <code>subject_style=transparent_human</code> 或 <code>source_actor</code>,均使用 <code>reconstruction_mode=similar</code>;后端会把这些帧视为同一个主体的证据,并锁定同一性别表现、年龄段、体型、材质、风格和视觉身份,避免六视图出现男女性别、老少年龄或样式混杂。音频结果下方是信息流复刻分镜工作台:顶部产品参考区是“同一产品素材池”,不限量上传产品图,不做不同产品身份判断;上传原图推荐长边 1200-2000px、短边至少 600px,但后端会统一生成最长边 1600px、JPEG 92 的 AI 工作副本,并回显尺寸、自动转换和风险标注;上传后按“套在脖子上的 U 形肩颈按摩仪”进行同一产品批量识别,左/右按佩戴者身体左右、上/下按佩戴方向,额外标注内外侧、开口方向、局部结构点、背景类型、用途标签、生成风险和备注,用户只检查备注,鼠标悬停通过固定浮层显示大图预览,能盖过滚动容器和分镜框架;缺视角补图失败时保留重试入口。脚本区在分镜行上方提供“作者想法”和“整片改写”,每行新口播文案可直接编辑并可单段 AI 改写,分镜时间和原内容列压缩为窄摘要列,把横向空间留给新口播、画面规划和视频候选;生成本条视频时使用当前编辑后的新口播文案。每条音频分镜纵向排列,行内从左到右串起原内容、新口播文案、画面规划/产品融入和 6 个候选视频槽;候选视频槽在宽屏下一排显示 6 个竖版预览,避免前面空旷、后面拥挤。单条生成会从全局选中关键帧或 12 张关键帧中取最贴近本句时间点的参考帧。单条生成会从产品素材池按分镜角色、视角优先级、用途标签、置信度和风险自动挑选最多 6 张相关产品图,不会把全部产品图提交给生视频模型,然后把产品坐标系、视角标注、方向、结构点和风险写入 Seedance 提示。<code>ModelTrace</code> 会在音频解析、产品识别/补图、相似主体 6 视图、脚本改写和单条生视频入口旁直接展示模型名;所有生图入口都显示并使用 <code>gpt-image-2</code>,没有其他图片模型 fallback;点击后用固定浮层展示模型链路、输入输出和回退逻辑。旧分镜卡、抽帧控制和视频生成组件仍保留在文件里,但当前主路径不渲染。</td></tr>
|
||||||
<tr><td><code>web/app/login/page.tsx</code></td><td>生产登录页:访问账号/访问密钥表单、保持登录、错误/成功状态;当前只在原版 Digital Oasis 动态背景上叠加一个组合登录框,桌面端左侧是动态角色,右侧是图标化登录表单;面板左上角展示官网 SKG 字标和中文“营销内容工作台”系统标识。</td></tr>
|
<tr><td><code>web/app/login/page.tsx</code></td><td>生产登录页:访问账号/访问密钥表单、保持登录、错误/成功状态;当前只在原版 Digital Oasis 动态背景上叠加一个组合登录框,桌面端左侧是动态角色,右侧是图标化登录表单;面板左上角展示官网 SKG 字标和中文“营销内容工作台”系统标识。</td></tr>
|
||||||
<tr><td><code>web/app/login/layout.tsx</code></td><td>登录路由专属 layout:覆盖全站默认网页标题和描述为空,避免 <code>/login</code> 继承工作台 metadata 后在页面源码里继续出现登录界面文字以外的文案。</td></tr>
|
<tr><td><code>web/app/login/layout.tsx</code></td><td>登录路由专属 layout:覆盖全站默认网页标题和描述为空,避免 <code>/login</code> 继承工作台 metadata 后在页面源码里继续出现登录界面文字以外的文案。</td></tr>
|
||||||
<tr><td><code>web/components/login/oasis-canvas.tsx</code></td><td>登录页全屏动态视觉层:用 iframe 直接承载下载包 <code>web/public/oasis-source/index.html</code> 的原 WebGPU / Three.js 草场源码;父级登录页只覆盖自己的文案和表单,并在捕获阶段把全局鼠标坐标同时用原生事件和 <code>postMessage</code> 转发给 iframe,避免登录面板或输入框遮挡时草地失去鼠标响应。</td></tr>
|
<tr><td><code>web/components/login/oasis-canvas.tsx</code></td><td>登录页全屏动态视觉层:用 iframe 直接承载下载包 <code>web/public/oasis-source/index.html</code> 的原 WebGPU / Three.js 草场源码;父级登录页只覆盖自己的文案和表单,并在捕获阶段把全局鼠标坐标同时用原生事件和 <code>postMessage</code> 转发给 iframe,避免登录面板或输入框遮挡时草地失去鼠标响应。</td></tr>
|
||||||
@@ -888,7 +888,7 @@ ProductRefStateItem {
|
|||||||
<tr><td>应用清洗</td><td><code>POST /cleanup/apply</code></td><td><code>applyCleanedFrame</code></td><td>物理覆盖 frames/{idx}.jpg,并备份原图。</td></tr>
|
<tr><td>应用清洗</td><td><code>POST /cleanup/apply</code></td><td><code>applyCleanedFrame</code></td><td>物理覆盖 frames/{idx}.jpg,并备份原图。</td></tr>
|
||||||
<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/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}/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>,如果用户手动选择了关键帧则只传已选帧,后端拼参考板。当前源视频工作区支持 <code>subject_style=transparent_human</code> 和 <code>subject_style=source_actor</code> 两种相似主体:透明骨架人会保持透明/半透明皮肤包裹可见白色骨架,普通真人会保持正常广告演员方向。两种模式都使用 <code>reconstruction_mode=similar</code>,最多读取 12 张参考帧,生成 6 张白底视图;后端强制使用 <code>gpt-image-2</code>,不再接受前端或环境变量切到其他图片模型,也不做 <code>gpt-image-1.5</code> / Gemini fallback;后端会加身份锁定约束,统一性别表现、年龄段、体型、材质、风格和视觉身份,避免六视图混成不同人物。前端白底视图缩略图和关键帧一样,鼠标停留会用顶层浮层放大预览,点击仍打开原图;后端每个 <code>view</code> 单独调用一次生图,并明确禁止六视图拼图、contact sheet、多主体、多面板、标签或对比排版,保证一个视角一张照片。</td></tr>
|
<tr><td>主体资产包</td><td><code>POST /elements/{element_id}/subject-assets</code><br><code>DELETE /elements/{element_id}/subject-assets/{asset_id}</code></td><td><code>generateSubjectAssets</code><br><code>deleteSubjectAsset</code></td><td>根据参考帧重新绘制一个统一主体资产包;前端默认把全部关键帧作为 <code>source_frame_indices</code>,如果用户手动选择了关键帧则只传已选帧,后端拼参考板。当前源视频工作区支持 <code>subject_style=transparent_human</code> 和 <code>subject_style=source_actor</code> 两种相似主体:透明骨架人会保持透明/半透明皮肤包裹可见白色骨架,普通真人会保持正常广告演员方向。两种模式都使用 <code>reconstruction_mode=similar</code>,最多读取 12 张参考帧,生成 6 张白底视图;后端强制使用 <code>gpt-image-2</code>,不再接受前端或环境变量切到其他图片模型,也不做 <code>gpt-image-1.5</code> / Gemini fallback;后端会加身份锁定约束,统一性别表现、年龄段、体型、材质、风格和视觉身份,避免六视图混成不同人物。如果参考帧是竖屏,prompt 会明确要求竖版 9:16 风格画布,落盘也按源帧纵横比归一化。前端白底视图缩略图和关键帧一样,鼠标停留会用顶层浮层放大预览,点击仍打开原图;后端每个 <code>view</code> 单独调用一次生图,并明确禁止六视图拼图、contact sheet、多主体、多面板、标签或对比排版,保证一个视角一张照片。<code>replace_views=true</code> 时会替换同一视角旧图;删除接口会移除对应 subject asset 记录并删除本地 jpg 文件。</td></tr>
|
||||||
<tr><td>首尾帧资产</td><td><code>POST /frames/{idx}/scene-asset</code></td><td><code>generateSceneAsset</code></td><td>同一接口兼容旧场景图和新首尾帧;新流程传 <code>asset_role=first_frame/last_frame</code>,后端走文字生图,参考帧只用于理解透明骨架人形象、比例、机位和光线,生成结果仍保存在 <code>scene_assets</code> 并自动填入产品融合镜头。</td></tr>
|
<tr><td>首尾帧资产</td><td><code>POST /frames/{idx}/scene-asset</code></td><td><code>generateSceneAsset</code></td><td>同一接口兼容旧场景图和新首尾帧;新流程传 <code>asset_role=first_frame/last_frame</code>,后端走文字生图,参考帧只用于理解透明骨架人形象、比例、机位和光线,生成结果仍保存在 <code>scene_assets</code> 并自动填入产品融合镜头。</td></tr>
|
||||||
<tr><td>产品图库</td><td><code>GET /product-library/skg</code></td><td><code>listProductLibrary</code></td><td>读取内置 SKG 白底图库 manifest,返回产品标题、品类、尺寸、白底评分和预览图 URL。</td></tr>
|
<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</code>、<code>POST /jobs/{id}/assets/product-library</code></td><td><code>uploadStoryboardAsset</code>、<code>copyProductLibraryAsset</code></td><td>上传产品图或把内置产品图库条目复制为当前 job 的普通 asset。后端统一生成最长边 1600px、JPEG 92 的 AI 工作副本,透明底铺白,过大/过小图片会在 <code>ImageRef.asset_meta</code> 里返回转换动作和风险;黑底/白底背景本身不强行转换。注意该接口只写图片文件,产品素材池列表另由 <code>PUT /jobs/{id}/product-refs</code> 持久化。</td></tr>
|
<tr><td>产品图入库到 job</td><td><code>POST /jobs/{id}/assets</code>、<code>POST /jobs/{id}/assets/product-library</code></td><td><code>uploadStoryboardAsset</code>、<code>copyProductLibraryAsset</code></td><td>上传产品图或把内置产品图库条目复制为当前 job 的普通 asset。后端统一生成最长边 1600px、JPEG 92 的 AI 工作副本,透明底铺白,过大/过小图片会在 <code>ImageRef.asset_meta</code> 里返回转换动作和风险;黑底/白底背景本身不强行转换。注意该接口只写图片文件,产品素材池列表另由 <code>PUT /jobs/{id}/product-refs</code> 持久化。</td></tr>
|
||||||
@@ -1004,6 +1004,19 @@ ProductRefStateItem {
|
|||||||
<h2>变更记录</h2>
|
<h2>变更记录</h2>
|
||||||
<p>这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。</p>
|
<p>这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。</p>
|
||||||
<div class="changelog">
|
<div class="changelog">
|
||||||
|
<article class="change">
|
||||||
|
<header>
|
||||||
|
<h3>2026-05-18 · 相似主体缩略图压缩并支持单张重生/删除</h3>
|
||||||
|
<span class="tag rose">UI</span>
|
||||||
|
<span class="tag violet">API</span>
|
||||||
|
<span class="tag cyan">Workflow</span>
|
||||||
|
</header>
|
||||||
|
<div class="body">
|
||||||
|
<p><strong>问题:</strong>关键帧和相似主体白底视图缩略图仍偏大;主体 6 视图只能整组重生,某一张不准时无法单独替换;白底视图也缺少删除入口。竖版视频参考生成的主体图还需要明确保持竖版画布。</p>
|
||||||
|
<p><strong>改动:</strong><code>SourceReferenceBuildPanel</code> 把关键帧网格改成更小的自适应列,把白底主体视图改成 9:16 固定小缩略图;每个主体视图新增重生和删除按钮。前端只展示每个 view 的最新一张;单张重生调用 <code>generateSubjectAssets</code> 并传 <code>views=[当前 view]</code> 与 <code>replace_views=true</code>,不会追加成第 7 张。后端 <code>GenerateSubjectAssetsReq</code> 新增 <code>replace_views</code>,并新增 <code>DELETE /subject-assets/{asset_id}</code>;竖屏参考帧会把 9:16 竖版画布要求写入 prompt,输出继续按源帧纵横比归一化。</p>
|
||||||
|
<p><strong>影响:</strong><code>web/components/ad-recreation-board.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">
|
<article class="change">
|
||||||
<header>
|
<header>
|
||||||
<h3>2026-05-18 · 生图模型统一锁定为 gpt-image-2</h3>
|
<h3>2026-05-18 · 生图模型统一锁定为 gpt-image-2</h3>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { type MouseEvent as ReactMouseEvent, type ReactNode, type RefObject, use
|
|||||||
import { createPortal } from "react-dom"
|
import { createPortal } from "react-dom"
|
||||||
import {
|
import {
|
||||||
AlertTriangle, Check, ChevronDown, Circle, Film, FileText, Image as ImageIcon, Info, Link2, Loader2,
|
AlertTriangle, Check, ChevronDown, Circle, Film, FileText, Image as ImageIcon, Info, Link2, Loader2,
|
||||||
Mic, Package, PanelRight, Play, Plus, Scissors, Sparkles, Trash2, Upload, Wand2,
|
Mic, Package, PanelRight, Play, Plus, RefreshCw, Scissors, Sparkles, Trash2, Upload, Wand2,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import {
|
import {
|
||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
analyzeProductViews,
|
analyzeProductViews,
|
||||||
apiAssetUrl,
|
apiAssetUrl,
|
||||||
cutoutElement,
|
cutoutElement,
|
||||||
|
deleteSubjectAsset,
|
||||||
effectiveFrameUrl,
|
effectiveFrameUrl,
|
||||||
generateProductAngleAsset,
|
generateProductAngleAsset,
|
||||||
generateSubjectAssets,
|
generateSubjectAssets,
|
||||||
@@ -97,6 +98,8 @@ type AudioStoryboardRow = {
|
|||||||
type ProductRefItem = ProductRefStateItem
|
type ProductRefItem = ProductRefStateItem
|
||||||
type SubjectStyleMode = "transparent_human" | "source_actor"
|
type SubjectStyleMode = "transparent_human" | "source_actor"
|
||||||
|
|
||||||
|
const SUBJECT_VIEW_ORDER = ["front", "three_quarter_left", "left", "back", "right", "three_quarter_right"]
|
||||||
|
|
||||||
type ModelTraceSpec = {
|
type ModelTraceSpec = {
|
||||||
title: string
|
title: string
|
||||||
model: string
|
model: string
|
||||||
@@ -1435,6 +1438,7 @@ function SourceReferenceBuildPanel({
|
|||||||
}) {
|
}) {
|
||||||
const [extracting, setExtracting] = useState(false)
|
const [extracting, setExtracting] = useState(false)
|
||||||
const [subjectBusy, setSubjectBusy] = useState(false)
|
const [subjectBusy, setSubjectBusy] = useState(false)
|
||||||
|
const [subjectAssetBusy, setSubjectAssetBusy] = useState<string | null>(null)
|
||||||
const [deletingFrame, setDeletingFrame] = useState<number | null>(null)
|
const [deletingFrame, setDeletingFrame] = useState<number | null>(null)
|
||||||
const [framePreview, setFramePreview] = useState<{ index: number; left: number; top: number } | null>(null)
|
const [framePreview, setFramePreview] = useState<{ index: number; left: number; top: number } | null>(null)
|
||||||
const [subjectAssetPreview, setSubjectAssetPreview] = useState<{ id: string; left: number; top: number } | null>(null)
|
const [subjectAssetPreview, setSubjectAssetPreview] = useState<{ id: string; left: number; top: number } | null>(null)
|
||||||
@@ -1453,8 +1457,22 @@ function SourceReferenceBuildPanel({
|
|||||||
return findSimilarActorSource(subjectReferenceFrames, frames)
|
return findSimilarActorSource(subjectReferenceFrames, frames)
|
||||||
}, [frames, subjectReferenceFrames])
|
}, [frames, subjectReferenceFrames])
|
||||||
const actorAssets = actorSource?.element.subject_assets ?? []
|
const actorAssets = actorSource?.element.subject_assets ?? []
|
||||||
|
const visibleActorAssets = useMemo(() => {
|
||||||
|
const latestByView = new Map<string, SubjectAsset>()
|
||||||
|
for (const asset of actorAssets) {
|
||||||
|
const current = latestByView.get(asset.view)
|
||||||
|
if (!current || (asset.created_at || 0) >= (current.created_at || 0)) {
|
||||||
|
latestByView.set(asset.view, asset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...latestByView.values()].sort((a, b) => {
|
||||||
|
const ai = SUBJECT_VIEW_ORDER.indexOf(a.view)
|
||||||
|
const bi = SUBJECT_VIEW_ORDER.indexOf(b.view)
|
||||||
|
return (ai === -1 ? 99 : ai) - (bi === -1 ? 99 : bi)
|
||||||
|
})
|
||||||
|
}, [actorAssets])
|
||||||
const previewFrame = framePreview ? frames.find((frame) => frame.index === framePreview.index) ?? null : null
|
const previewFrame = framePreview ? frames.find((frame) => frame.index === framePreview.index) ?? null : null
|
||||||
const previewSubjectAsset = subjectAssetPreview ? actorAssets.find((asset) => asset.id === subjectAssetPreview.id) ?? null : null
|
const previewSubjectAsset = subjectAssetPreview ? visibleActorAssets.find((asset) => asset.id === subjectAssetPreview.id) ?? null : null
|
||||||
const referenceCountLabel = selectedReferenceFrames.length
|
const referenceCountLabel = selectedReferenceFrames.length
|
||||||
? `使用已选 ${selectedReferenceFrames.length} 张`
|
? `使用已选 ${selectedReferenceFrames.length} 张`
|
||||||
: frames.length
|
: frames.length
|
||||||
@@ -1512,6 +1530,7 @@ function SourceReferenceBuildPanel({
|
|||||||
source_frame_indices: subjectReferenceFrames.slice(0, 12).map((frame) => frame.index),
|
source_frame_indices: subjectReferenceFrames.slice(0, 12).map((frame) => frame.index),
|
||||||
views: ["front", "back", "left", "right", "three_quarter_left", "three_quarter_right"],
|
views: ["front", "back", "left", "right", "three_quarter_left", "three_quarter_right"],
|
||||||
prompt: buildSimilarSubjectPrompt(subjectStyle, subjectDirection),
|
prompt: buildSimilarSubjectPrompt(subjectStyle, subjectDirection),
|
||||||
|
replace_views: true,
|
||||||
})
|
})
|
||||||
onJobUpdate(updated)
|
onJobUpdate(updated)
|
||||||
toast.success("相似主体 6 张白底视图已生成")
|
toast.success("相似主体 6 张白底视图已生成")
|
||||||
@@ -1532,6 +1551,47 @@ function SourceReferenceBuildPanel({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const regenerateSubjectAsset = async (asset: SubjectAsset) => {
|
||||||
|
if (!actorSource) return
|
||||||
|
setSubjectAssetBusy(`regen:${asset.id}`)
|
||||||
|
try {
|
||||||
|
const sourceIndices = asset.source_frame_indices?.length
|
||||||
|
? asset.source_frame_indices
|
||||||
|
: subjectReferenceFrames.slice(0, 12).map((frame) => frame.index)
|
||||||
|
const updated = await generateSubjectAssets(job.id, actorSource.frame.index, actorSource.element.id, {
|
||||||
|
subject_kind: "living",
|
||||||
|
subject_style: subjectStyle,
|
||||||
|
reconstruction_mode: "similar",
|
||||||
|
background: asset.background || "white",
|
||||||
|
size: asset.size || "1024",
|
||||||
|
source_frame_indices: sourceIndices,
|
||||||
|
views: [asset.view],
|
||||||
|
prompt: buildSimilarSubjectPrompt(subjectStyle, subjectDirection),
|
||||||
|
replace_views: true,
|
||||||
|
})
|
||||||
|
onJobUpdate(updated)
|
||||||
|
toast.success("已重新生成这张主体视图")
|
||||||
|
} catch (e) {
|
||||||
|
toast.error("单张主体视图重生失败:" + (e instanceof Error ? e.message : String(e)))
|
||||||
|
} finally {
|
||||||
|
setSubjectAssetBusy(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteActorAsset = async (asset: SubjectAsset) => {
|
||||||
|
if (!actorSource) return
|
||||||
|
setSubjectAssetBusy(`delete:${asset.id}`)
|
||||||
|
try {
|
||||||
|
const updated = await deleteSubjectAsset(job.id, actorSource.frame.index, actorSource.element.id, asset.id)
|
||||||
|
onJobUpdate(updated)
|
||||||
|
toast.success("主体视图已删除")
|
||||||
|
} catch (e) {
|
||||||
|
toast.error("主体视图删除失败:" + (e instanceof Error ? e.message : String(e)))
|
||||||
|
} finally {
|
||||||
|
setSubjectAssetBusy(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const updateFramePreviewPosition = (event: ReactMouseEvent<HTMLDivElement>, frameIndex: number) => {
|
const updateFramePreviewPosition = (event: ReactMouseEvent<HTMLDivElement>, frameIndex: number) => {
|
||||||
const margin = 16
|
const margin = 16
|
||||||
const previewWidth = Math.min(340, window.innerWidth - margin * 2)
|
const previewWidth = Math.min(340, window.innerWidth - margin * 2)
|
||||||
@@ -1635,7 +1695,7 @@ function SourceReferenceBuildPanel({
|
|||||||
<span className="text-[10.5px] text-white/30">不勾选则默认用全部帧</span>
|
<span className="text-[10.5px] text-white/30">不勾选则默认用全部帧</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-2 grid grid-cols-6 gap-1.5 md:grid-cols-8 xl:grid-cols-12 2xl:grid-cols-16">
|
<div className="mt-2 grid grid-cols-[repeat(auto-fill,minmax(38px,1fr))] gap-1">
|
||||||
{frames.map((frame, index) => {
|
{frames.map((frame, index) => {
|
||||||
const selected = selectedFrames.has(frame.index)
|
const selected = selectedFrames.has(frame.index)
|
||||||
return (
|
return (
|
||||||
@@ -1715,7 +1775,7 @@ function SourceReferenceBuildPanel({
|
|||||||
placeholder="统一方向:如年轻女性 / 更运动 / 更高级"
|
placeholder="统一方向:如年轻女性 / 更运动 / 更高级"
|
||||||
className="h-7 w-[240px] min-w-[180px] rounded-md border border-white/10 bg-black/35 px-2 text-[10.5px] text-white outline-none placeholder:text-white/28 focus:border-cyan-300/50"
|
className="h-7 w-[240px] min-w-[180px] rounded-md border border-white/10 bg-black/35 px-2 text-[10.5px] text-white outline-none placeholder:text-white/28 focus:border-cyan-300/50"
|
||||||
/>
|
/>
|
||||||
<span>{actorAssets.length}/6</span>
|
<span>{visibleActorAssets.length}/6</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => void generateSimilarActor()}
|
onClick={() => void generateSimilarActor()}
|
||||||
@@ -1727,23 +1787,60 @@ function SourceReferenceBuildPanel({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{actorAssets.length ? (
|
{visibleActorAssets.length ? (
|
||||||
<div className="grid grid-cols-6 gap-1.5">
|
<div className="flex flex-wrap gap-1.5">
|
||||||
{actorAssets.slice(-6).map((asset) => (
|
{visibleActorAssets.map((asset) => {
|
||||||
<a
|
const busyMode = subjectAssetBusy?.endsWith(asset.id) ? subjectAssetBusy.split(":")[0] : ""
|
||||||
|
return (
|
||||||
|
<div
|
||||||
key={asset.id}
|
key={asset.id}
|
||||||
href={subjectAssetUrl(job, asset)}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
onMouseEnter={(event) => updateSubjectAssetPreviewPosition(event, asset.id)}
|
onMouseEnter={(event) => updateSubjectAssetPreviewPosition(event, asset.id)}
|
||||||
onMouseMove={(event) => updateSubjectAssetPreviewPosition(event, asset.id)}
|
onMouseMove={(event) => updateSubjectAssetPreviewPosition(event, asset.id)}
|
||||||
onMouseLeave={() => setSubjectAssetPreview(null)}
|
onMouseLeave={() => setSubjectAssetPreview(null)}
|
||||||
className="h-20 overflow-hidden rounded border border-white/10 bg-white transition hover:border-cyan-200/70 2xl:h-24"
|
className="group relative aspect-[9/16] w-12 overflow-hidden rounded border border-white/10 bg-white transition hover:border-cyan-200/70 2xl:w-14"
|
||||||
title={asset.label || asset.view}
|
title={asset.label || asset.view}
|
||||||
>
|
>
|
||||||
<img src={subjectAssetUrl(job, asset)} alt={asset.label || asset.view} className="h-full w-full object-contain" />
|
<a
|
||||||
</a>
|
href={subjectAssetUrl(job, asset)}
|
||||||
))}
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="absolute inset-0"
|
||||||
|
>
|
||||||
|
<img src={subjectAssetUrl(job, asset)} alt={asset.label || asset.view} className="h-full w-full object-contain" />
|
||||||
|
</a>
|
||||||
|
<div className="absolute bottom-1 right-1 flex flex-col gap-1 opacity-0 transition group-hover:opacity-100 group-focus-within:opacity-100">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
void regenerateSubjectAsset(asset)
|
||||||
|
}}
|
||||||
|
disabled={!!subjectAssetBusy}
|
||||||
|
className="inline-flex h-5 w-5 items-center justify-center rounded-full border border-cyan-100/35 bg-black/78 text-cyan-100 transition hover:border-cyan-100/70 hover:bg-cyan-500/25 focus:outline-none focus:ring-1 focus:ring-cyan-100/70 disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
|
aria-label={`重新生成${asset.label || asset.view}`}
|
||||||
|
title="重新生成这一张"
|
||||||
|
>
|
||||||
|
{busyMode === "regen" ? <Loader2 className="h-3 w-3 animate-spin" /> : <RefreshCw className="h-3 w-3" />}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
void deleteActorAsset(asset)
|
||||||
|
}}
|
||||||
|
disabled={!!subjectAssetBusy}
|
||||||
|
className="inline-flex h-5 w-5 items-center justify-center rounded-full border border-rose-100/35 bg-black/78 text-rose-100 transition hover:border-rose-100/70 hover:bg-rose-500/25 focus:outline-none focus:ring-1 focus:ring-rose-100/70 disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
|
aria-label={`删除${asset.label || asset.view}`}
|
||||||
|
title="删除这一张"
|
||||||
|
>
|
||||||
|
{busyMode === "delete" ? <Loader2 className="h-3 w-3 animate-spin" /> : <Trash2 className="h-3 w-3" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="rounded border border-dashed border-white/12 px-2 py-2 text-[10.5px] leading-snug text-white/32">
|
<div className="rounded border border-dashed border-white/12 px-2 py-2 text-[10.5px] leading-snug text-white/32">
|
||||||
|
|||||||
@@ -1066,6 +1066,7 @@ export async function generateSubjectAssets(
|
|||||||
subject_style?: "transparent_human" | "source_actor"
|
subject_style?: "transparent_human" | "source_actor"
|
||||||
reconstruction_mode?: "same" | "similar"
|
reconstruction_mode?: "same" | "similar"
|
||||||
prompt?: string
|
prompt?: string
|
||||||
|
replace_views?: boolean
|
||||||
} = {},
|
} = {},
|
||||||
): Promise<Job> {
|
): Promise<Job> {
|
||||||
const res = await fetch(`${API_BASE}/jobs/${jobId}/frames/${frameIdx}/elements/${elementId}/subject-assets`, {
|
const res = await fetch(`${API_BASE}/jobs/${jobId}/frames/${frameIdx}/elements/${elementId}/subject-assets`, {
|
||||||
@@ -1081,6 +1082,7 @@ export async function generateSubjectAssets(
|
|||||||
subject_style: body.subject_style ?? "transparent_human",
|
subject_style: body.subject_style ?? "transparent_human",
|
||||||
reconstruction_mode: body.reconstruction_mode ?? "same",
|
reconstruction_mode: body.reconstruction_mode ?? "same",
|
||||||
prompt: body.prompt ?? "",
|
prompt: body.prompt ?? "",
|
||||||
|
replace_views: body.replace_views ?? false,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
@@ -1089,3 +1091,19 @@ export async function generateSubjectAssets(
|
|||||||
}
|
}
|
||||||
return res.json()
|
return res.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function deleteSubjectAsset(
|
||||||
|
jobId: string,
|
||||||
|
frameIdx: number,
|
||||||
|
elementId: string,
|
||||||
|
assetId: string,
|
||||||
|
): Promise<Job> {
|
||||||
|
const res = await fetch(`${API_BASE}/jobs/${jobId}/frames/${frameIdx}/elements/${elementId}/subject-assets/${assetId}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
})
|
||||||
|
if (!res.ok) {
|
||||||
|
const txt = await res.text().catch(() => "")
|
||||||
|
throw new Error(`deleteSubjectAsset ${res.status} ${txt.slice(0, 300)}`)
|
||||||
|
}
|
||||||
|
return res.json()
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user