auto-save 2026-05-13 13:08 (~5)
This commit is contained in:
@@ -1516,6 +1516,19 @@
|
||||
"type": "session-heartbeat",
|
||||
"message": "Claude 会话活跃 · 最近命令:claude · 1 项未提交变更 · 最近提交:auto-save 2026-05-13 12:57 (~2)",
|
||||
"files_changed": 1
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-13T13:03:04+08:00",
|
||||
"type": "commit",
|
||||
"message": "auto-save 2026-05-13 13:02 (~1)",
|
||||
"hash": "b6c9e0c",
|
||||
"files_changed": 1
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-13T05:07:38Z",
|
||||
"type": "session-heartbeat",
|
||||
"message": "Claude 会话活跃 · 最近命令:claude · 5 项未提交变更 · 最近提交:auto-save 2026-05-13 13:02 (~1)",
|
||||
"files_changed": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
66
api/main.py
66
api/main.py
@@ -1261,14 +1261,11 @@ def delete_element(job_id: str, idx: int, element_id: str) -> Job:
|
||||
return job
|
||||
|
||||
|
||||
class CutoutReq(BaseModel):
|
||||
background: Literal["white", "black"] = "white"
|
||||
|
||||
|
||||
@app.post("/jobs/{job_id}/frames/{idx}/elements/{element_id}/cutout", response_model=Job)
|
||||
def cutout_element(job_id: str, idx: int, element_id: str, req: CutoutReq | None = None) -> Job:
|
||||
"""单元素抠图:调 nano-banana image edit,输出纯白底 / 纯黑底元素图。
|
||||
注:Gemini 输出 JPEG 不支持真 alpha,因此用纯色背景。"""
|
||||
def cutout_element(job_id: str, idx: int, element_id: str) -> Job:
|
||||
"""元素裁切:直接 PIL crop 原帧 region 区域(不调模型,原汁原味保留表情/形体)。
|
||||
只支持 region 模式元素;vision auto / 纯手动文字元素没有坐标,无法裁切。"""
|
||||
from PIL import Image as _PILImage
|
||||
job = JOBS.get(job_id)
|
||||
if not job:
|
||||
raise HTTPException(404, "job not found")
|
||||
@@ -1278,48 +1275,38 @@ def cutout_element(job_id: str, idx: int, element_id: str, req: CutoutReq | None
|
||||
el = next((e for e in frame.elements if e.id == element_id), None)
|
||||
if not el:
|
||||
raise HTTPException(404, "element not found")
|
||||
if not el.region:
|
||||
raise HTTPException(400, "该元素没有坐标,无法裁切(仅画框元素支持)")
|
||||
|
||||
# 优先用 cleaned 版作 reference(已去掉 logo / 水印干扰),fallback 原图
|
||||
# 优先用 cleaned 版作 source
|
||||
cleaned_path = job_dir(job_id) / "cleaned" / f"{idx:03d}.jpg"
|
||||
src = cleaned_path if cleaned_path.exists() else job_dir(job_id) / "frames" / f"{idx:03d}.jpg"
|
||||
if not src.exists():
|
||||
raise HTTPException(404, "source frame file missing")
|
||||
|
||||
background = (req.background if req else "white") or "white"
|
||||
bg_phrase = f"pure {background}"
|
||||
|
||||
target = (el.name_en or el.name_zh).strip()
|
||||
region_phrase = _region_to_phrase(el.region) if el.region else ""
|
||||
if region_phrase:
|
||||
prompt = (
|
||||
f"Extract whatever is in the {region_phrase} part of the image as a standalone asset. "
|
||||
f"Place it on a {bg_phrase} background, isolated, no other objects."
|
||||
)
|
||||
else:
|
||||
position_hint = f" Located in the {el.position} area." if el.position else ""
|
||||
prompt = (
|
||||
f"Extract the {target} from this image as a standalone asset.{position_hint} "
|
||||
f"Place it on a {bg_phrase} background, isolated, no other objects."
|
||||
)
|
||||
# 模型轮换:nano-banana-pro (首选) → gemini-2.5-flash-image (兜底确认可用)
|
||||
# gemini-3.1-flash-image-preview 不支持 i2i (404),剔除
|
||||
models = [
|
||||
IMAGE_MODEL,
|
||||
"gemini-2.5-flash-image",
|
||||
]
|
||||
try:
|
||||
img_bytes, _mode = _image_edit_call(
|
||||
src, prompt, models=models, fallback_text=False, max_attempts=3,
|
||||
)
|
||||
except RuntimeError as e:
|
||||
raise HTTPException(500, f"cutout failed: {e}")
|
||||
im = _PILImage.open(src).convert("RGB")
|
||||
W, H = im.size
|
||||
r = el.region
|
||||
x = max(0.0, min(1.0, float(r.get("x", 0))))
|
||||
y = max(0.0, min(1.0, float(r.get("y", 0))))
|
||||
w = max(0.0, min(1.0 - x, float(r.get("w", 0))))
|
||||
h = max(0.0, min(1.0 - y, float(r.get("h", 0))))
|
||||
left, top = int(x * W), int(y * H)
|
||||
right, bottom = int((x + w) * W), int((y + h) * H)
|
||||
if right - left < 4 or bottom - top < 4:
|
||||
raise HTTPException(400, "region 太小,无法裁切")
|
||||
cropped = im.crop((left, top, right, bottom))
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(500, f"crop failed: {e}")
|
||||
|
||||
out_dir = job_dir(job_id) / "elements"
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
# 实际是 JPEG bytes,文件用 .jpg 真名
|
||||
out_path = out_dir / f"{idx:03d}_{element_id}.jpg"
|
||||
out_path.write_bytes(img_bytes)
|
||||
# 旧版的 .png 文件(错命名为 .png 的 JPEG)也清理掉
|
||||
cropped.save(out_path, format="JPEG", quality=92)
|
||||
# 清理旧版的 .png(旧版"抠图"产物)
|
||||
old_png = out_dir / f"{idx:03d}_{element_id}.png"
|
||||
if old_png.exists():
|
||||
try: old_png.unlink()
|
||||
@@ -1331,9 +1318,8 @@ def cutout_element(job_id: str, idx: int, element_id: str, req: CutoutReq | None
|
||||
for e in f.elements:
|
||||
if e.id == element_id:
|
||||
e.cutout_id = element_id
|
||||
e.cutout_background = background
|
||||
new_frames.append(f)
|
||||
update(job, frames=new_frames, message=f"抠图完成 · {el.name_zh}({background} 底)")
|
||||
update(job, frames=new_frames, message=f"裁切完成 · {el.name_zh}")
|
||||
return job
|
||||
|
||||
|
||||
|
||||
@@ -220,14 +220,14 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
|
||||
}
|
||||
}
|
||||
|
||||
const handleCutout = async (id: string, background: "white" | "black" = "white") => {
|
||||
const handleCutout = async (id: string) => {
|
||||
setCuttingId(id)
|
||||
try {
|
||||
const updated = await cutoutElement(jobId, f.index, id, background)
|
||||
const updated = await cutoutElement(jobId, f.index, id)
|
||||
onJobUpdate?.(updated)
|
||||
toast.success(`抠图完成(${background === "white" ? "白底" : "黑底"})`)
|
||||
toast.success("裁切完成")
|
||||
} catch (e) {
|
||||
toast.error("抠图失败:" + (e instanceof Error ? e.message : String(e)))
|
||||
toast.error("裁切失败:" + (e instanceof Error ? e.message : String(e)))
|
||||
} finally {
|
||||
setCuttingId(null)
|
||||
}
|
||||
@@ -609,29 +609,29 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
|
||||
{elements.map((e) => {
|
||||
const isCutting = cuttingId === e.id
|
||||
const hasCutout = !!e.cutout_id
|
||||
const bg = e.cutout_background ?? "white"
|
||||
const hasRegion = !!e.region
|
||||
return (
|
||||
<div
|
||||
key={e.id}
|
||||
className="group/c relative rounded-md bg-white/[0.04] border border-white/10 px-2.5 py-1.5 flex items-start gap-2"
|
||||
>
|
||||
{/* 抠图缩略图 / 占位(真实底色:white/black) */}
|
||||
{/* 裁切缩略图 / 占位 */}
|
||||
<div
|
||||
className="flex-shrink-0 rounded border border-white/8 flex items-center justify-center overflow-hidden"
|
||||
style={{ width: 36, height: 36, background: hasCutout ? (bg === "black" ? "#000" : "#fff") : "rgba(0,0,0,0.4)" }}
|
||||
className="flex-shrink-0 rounded border border-white/8 flex items-center justify-center overflow-hidden bg-black/40"
|
||||
style={{ width: 36, height: 36 }}
|
||||
>
|
||||
{hasCutout ? (
|
||||
<a
|
||||
href={cutoutUrl(jobId, f.index, e.id)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
title={`点击查看 · ${bg === "white" ? "白底" : "黑底"}`}
|
||||
title="点击查看原图"
|
||||
className="block w-full h-full"
|
||||
>
|
||||
<img
|
||||
src={cutoutUrl(jobId, f.index, e.id)}
|
||||
alt={e.name_zh}
|
||||
className="w-full h-full object-contain"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</a>
|
||||
) : (
|
||||
@@ -645,10 +645,8 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
|
||||
{e.source === "auto" && (
|
||||
<span className="text-[8.5px] text-pink-300/70 font-mono uppercase">auto</span>
|
||||
)}
|
||||
{hasCutout && (
|
||||
<span className={`text-[8.5px] font-mono uppercase ${bg === "white" ? "text-white/80" : "text-white/50"}`}>
|
||||
{bg}
|
||||
</span>
|
||||
{e.source === "region" && (
|
||||
<span className="text-[8.5px] text-cyan-300/80 font-mono uppercase">box</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-0.5 text-[10px] font-mono leading-tight truncate text-white/45">
|
||||
@@ -656,38 +654,24 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 底色 toggle(小) — 选 white/black 后点抠图 */}
|
||||
<div className="shrink-0 inline-flex rounded overflow-hidden border border-white/15">
|
||||
{/* 裁切按钮 — 只在有 region 时可用 */}
|
||||
{hasRegion ? (
|
||||
<button
|
||||
onClick={() => handleCutout(e.id, "white")}
|
||||
onClick={() => handleCutout(e.id)}
|
||||
disabled={isCutting}
|
||||
title="白底抠图"
|
||||
className={`text-[9.5px] px-1.5 py-0.5 transition ${
|
||||
hasCutout && bg === "white"
|
||||
? "bg-white text-black"
|
||||
: "bg-white/[0.05] text-white/70 hover:bg-white/15"
|
||||
title={hasCutout ? "重新裁切原图区域" : "从原图裁切该 region 区域(保留原表情/形体)"}
|
||||
className={`shrink-0 text-[10px] px-1.5 py-0.5 rounded inline-flex items-center gap-1 transition disabled:opacity-50 disabled:cursor-not-allowed ${
|
||||
hasCutout
|
||||
? "bg-emerald-500/20 text-emerald-200 hover:bg-emerald-500/30"
|
||||
: "bg-cyan-500/30 text-white/90 hover:bg-cyan-500/50"
|
||||
}`}
|
||||
>W</button>
|
||||
<button
|
||||
onClick={() => handleCutout(e.id, "black")}
|
||||
disabled={isCutting}
|
||||
title="黑底抠图"
|
||||
className={`text-[9.5px] px-1.5 py-0.5 transition ${
|
||||
hasCutout && bg === "black"
|
||||
? "bg-black text-white"
|
||||
: "bg-black/30 text-white/70 hover:bg-black/60"
|
||||
}`}
|
||||
>B</button>
|
||||
</div>
|
||||
|
||||
{/* 抠图状态指示(不再是按钮,因为 W/B toggle 即触发) */}
|
||||
{isCutting ? (
|
||||
<span className="shrink-0 text-[10px] px-1.5 py-0.5 rounded inline-flex items-center gap-1 bg-violet-500/30 text-white/90">
|
||||
<Loader2 className="h-2.5 w-2.5 animate-spin" /> 抠图中
|
||||
</span>
|
||||
) : !hasCutout ? (
|
||||
<span className="shrink-0 text-[9.5px] text-white/35 self-center">未抠</span>
|
||||
) : null}
|
||||
>
|
||||
{isCutting ? <Loader2 className="h-2.5 w-2.5 animate-spin" /> : <Sparkle className="h-2.5 w-2.5" />}
|
||||
{isCutting ? "裁切中" : hasCutout ? "重裁" : "裁切"}
|
||||
</button>
|
||||
) : (
|
||||
<span className="shrink-0 text-[9.5px] text-white/35 self-center" title="该元素仅文字描述 · 在原图上画框才能裁切">仅文字</span>
|
||||
)}
|
||||
|
||||
{/* 删除 */}
|
||||
<button
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
Mic, Languages, FileEdit, Sparkles, Film, FileVideo, Loader2, Plus, X, LayoutGrid,
|
||||
} from "lucide-react"
|
||||
import { NodeShell, type NodeStatus, type NodeKind } from "./node-shell"
|
||||
import { type Job, frameUrl, effectiveFrameUrl, videoUrl, generatedImageUrl } from "@/lib/api"
|
||||
import { type Job, frameUrl, effectiveFrameUrl, videoUrl, generatedImageUrl, cutoutUrl } from "@/lib/api"
|
||||
|
||||
export interface NodeData {
|
||||
job: Job | null // 当前 active job
|
||||
@@ -588,107 +588,74 @@ const IMAGEGEN_WIDTH = 320
|
||||
export function ImageGenNode({ data, selected }: any) {
|
||||
const d: NodeData = data
|
||||
const job = d?.job
|
||||
const selectedIdxs = Array.from(d?.selectedFrames ?? []).sort((a: number, b: number) => a - b) as number[]
|
||||
|
||||
// 每个 selected frame 取一张代表图:优先 selected gen,否则最新一张
|
||||
const previews = selectedIdxs
|
||||
.map((idx) => {
|
||||
const f = job?.frames.find((x) => x.index === idx)
|
||||
if (!f || !f.generated_images || f.generated_images.length === 0) return null
|
||||
const sel = f.generated_images.find((g) => g.selected)
|
||||
const pick = sel ?? f.generated_images[f.generated_images.length - 1]
|
||||
return { frameIdx: idx, gen: pick, hasSelected: !!sel, total: f.generated_images.length }
|
||||
})
|
||||
.filter((p): p is { frameIdx: number; gen: NonNullable<ReturnType<typeof Object>>; hasSelected: boolean; total: number } => p !== null)
|
||||
// 上方浮条 = 所有 frame 的 elements crop("分镜头编排"的输入素材)
|
||||
type ElPreview = { frameIdx: number; elementId: string; name: string }
|
||||
const elementCrops: ElPreview[] = job
|
||||
? job.frames.flatMap((f) =>
|
||||
(f.elements ?? [])
|
||||
.filter((e) => !!e.cutout_id)
|
||||
.map((e) => ({ frameIdx: f.index, elementId: e.id, name: e.name_zh })),
|
||||
)
|
||||
: []
|
||||
|
||||
const totalGens = job?.frames.reduce((sum, f) => sum + (f.generated_images?.length ?? 0), 0) ?? 0
|
||||
const selectedCount = previews.filter((p) => p.hasSelected).length
|
||||
|
||||
const status: NodeStatus = !job ? "pending" : totalGens > 0 ? "done" : "pending"
|
||||
const aspect = job && job.height > 0 ? `${job.width}/${job.height}` : "9/16"
|
||||
const totalElements = elementCrops.length
|
||||
const status: NodeStatus = !job ? "pending" : totalElements > 0 ? "done" : "pending"
|
||||
|
||||
return (
|
||||
<div className="relative" style={{ width: IMAGEGEN_WIDTH }}>
|
||||
{/* 节点上方:每帧 1 张缩略图 */}
|
||||
{previews.length > 0 && job && (
|
||||
{/* 节点上方:所有元素 crop 图(编排输入素材) */}
|
||||
{elementCrops.length > 0 && job && (
|
||||
<div
|
||||
className="absolute left-0 right-0 grid grid-cols-5 gap-1.5"
|
||||
className="absolute left-0 right-0 grid grid-cols-6 gap-1.5"
|
||||
style={{ bottom: "calc(100% + 12px)" }}
|
||||
>
|
||||
{previews.map((p) => (
|
||||
{elementCrops.map((p) => (
|
||||
<div
|
||||
key={p.frameIdx}
|
||||
className={`group relative rounded-md border transition shadow-lg hover:-translate-y-0.5 ${
|
||||
p.hasSelected
|
||||
? "border-emerald-400 ring-2 ring-emerald-400/60"
|
||||
: "border-pink-300/40 dark:border-pink-300/30"
|
||||
}`}
|
||||
style={{ aspectRatio: aspect }}
|
||||
key={`${p.frameIdx}_${p.elementId}`}
|
||||
className="group relative rounded-md border border-violet-300/40 dark:border-violet-300/30 transition shadow-lg hover:-translate-y-0.5 bg-black/40 overflow-hidden"
|
||||
style={{ aspectRatio: "1/1" }}
|
||||
>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); d.onOpenPanel?.("imagegen") }}
|
||||
title={`分镜 ${p.frameIdx + 1} · ${p.total} 张${p.hasSelected ? " · 已选用" : ""} · 打开「分镜头编排」`}
|
||||
className="absolute inset-0 w-full h-full"
|
||||
>
|
||||
<img
|
||||
src={generatedImageUrl(job.id, p.frameIdx, p.gen.id)}
|
||||
alt={`gen ${p.gen.id}`}
|
||||
className="absolute inset-0 w-full h-full object-cover rounded-md"
|
||||
/>
|
||||
{p.total > 1 && (
|
||||
<div className="absolute top-0 left-0 bg-black/70 text-white text-[8.5px] font-mono px-1 py-0.5 leading-none rounded-tl-md rounded-br">
|
||||
{p.total}
|
||||
</div>
|
||||
)}
|
||||
{p.hasSelected && (
|
||||
<div className="absolute inset-0 bg-emerald-400/15 rounded-md pointer-events-none" />
|
||||
)}
|
||||
|
||||
{/* Hover 大图预览 */}
|
||||
<div
|
||||
className="pointer-events-none absolute opacity-0 group-hover:opacity-100 scale-95 group-hover:scale-100 transition-all duration-200 z-[60]"
|
||||
style={{
|
||||
bottom: "calc(100% + 10px)",
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
transformOrigin: "bottom center",
|
||||
}}
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); d.onOpenPanel?.("imagegen") }}
|
||||
title={`${p.name} · 来自分镜 ${p.frameIdx + 1} · 打开「分镜头编排」`}
|
||||
className="absolute inset-0 w-full h-full"
|
||||
>
|
||||
<div className="rounded-2xl overflow-hidden border border-white/25 bg-black" style={{ boxShadow: "0 40px 100px -20px rgba(0,0,0,0.85), 0 0 0 1px rgba(255,255,255,0.06)" }}>
|
||||
<img
|
||||
src={generatedImageUrl(job.id, p.frameIdx, p.gen.id)}
|
||||
alt={`preview ${p.gen.id}`}
|
||||
className="block"
|
||||
style={{
|
||||
width: IMAGEGEN_WIDTH * 2,
|
||||
maxWidth: "min(720px, 80vw)",
|
||||
height: "auto",
|
||||
maxHeight: "82vh",
|
||||
objectFit: "contain",
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center justify-between px-3 py-2 bg-black/70 backdrop-blur-md">
|
||||
<span className="text-white text-[12.5px] font-medium">分镜 {p.frameIdx + 1} · 生成图</span>
|
||||
<span className="text-white/60 text-[11px] font-mono">{p.gen.mode === "edit" ? "i2i" : "text"} · {p.total} 张</span>
|
||||
<img
|
||||
src={cutoutUrl(job.id, p.frameIdx, p.elementId)}
|
||||
alt={p.name}
|
||||
className="absolute inset-0 w-full h-full object-contain"
|
||||
/>
|
||||
|
||||
{/* Hover 大图预览 */}
|
||||
<div
|
||||
className="pointer-events-none absolute opacity-0 group-hover:opacity-100 scale-95 group-hover:scale-100 transition-all duration-200 z-[60]"
|
||||
style={{
|
||||
bottom: "calc(100% + 10px)",
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
transformOrigin: "bottom center",
|
||||
}}
|
||||
>
|
||||
<div className="rounded-2xl overflow-hidden border border-white/25 bg-black" style={{ boxShadow: "0 40px 100px -20px rgba(0,0,0,0.85), 0 0 0 1px rgba(255,255,255,0.06)" }}>
|
||||
<img
|
||||
src={cutoutUrl(job.id, p.frameIdx, p.elementId)}
|
||||
alt={`preview ${p.elementId}`}
|
||||
className="block"
|
||||
style={{
|
||||
maxWidth: "min(560px, 70vw)",
|
||||
height: "auto",
|
||||
maxHeight: "70vh",
|
||||
objectFit: "contain",
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center justify-between px-3 py-2 bg-black/70 backdrop-blur-md">
|
||||
<span className="text-white text-[12.5px] font-medium">{p.name}</span>
|
||||
<span className="text-white/60 text-[11px] font-mono">分镜 {p.frameIdx + 1}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
{/* 删除按钮:hover 时右上角浮出 */}
|
||||
{d.onDeleteGenerated && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
if (confirm(`删除分镜 ${p.frameIdx + 1} 的这张生成图?`)) {
|
||||
d.onDeleteGenerated?.(p.frameIdx, p.gen.id)
|
||||
}
|
||||
}}
|
||||
title="删除该生成图"
|
||||
className="absolute top-1 right-1 h-5 w-5 rounded-full bg-black/70 backdrop-blur text-white/80 hover:bg-rose-500 hover:text-white inline-flex items-center justify-center opacity-0 group-hover:opacity-100 transition z-[70]"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -698,23 +665,23 @@ export function ImageGenNode({ data, selected }: any) {
|
||||
type="ai" status={status}
|
||||
icon={<LayoutGrid className="h-4 w-4" />}
|
||||
title="分镜头编排 · Storyboard"
|
||||
subtitle={`STEP 6 · 接元素 + 场景 ${totalGens > 0 ? `· ${totalGens} 张` : ""}`}
|
||||
subtitle={`STEP 6 · 接元素 + 场景${totalElements > 0 ? ` · ${totalElements} 个元素` : ""}`}
|
||||
width={IMAGEGEN_WIDTH}
|
||||
selected={selected}
|
||||
>
|
||||
{totalGens > 0 ? (
|
||||
{totalElements > 0 ? (
|
||||
<div className="text-[11.5px] leading-relaxed text-[var(--text-soft)]">
|
||||
已生成 <span className="text-[var(--text-strong)] font-medium">{totalGens}</span> 张 · 选用 {selectedCount}/{previews.length}
|
||||
素材:<span className="text-[var(--text-strong)] font-medium">{totalElements}</span> 个元素 + 干净版场景
|
||||
<br />
|
||||
<span className="text-[10.5px] text-[var(--text-faint)]">
|
||||
上方缩略图点击展开编排 · 多视角 / 风格融合 / 布局调整在此完成
|
||||
上方缩略图点击进入编排 · 多视角 / 风格融合 / 布局在此完成(Phase 2)
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-[11.5px] text-[var(--text-faint)] leading-relaxed">
|
||||
<span className="text-[var(--text-strong)]">编排素材待接入</span>
|
||||
<br />
|
||||
<span className="text-[10.5px]">关键帧节点提取的元素 + 干净版场景图 → 这里做多视角 / 风格融合 / 分镜布局</span>
|
||||
<span className="text-[10.5px]">到关键帧节点画框 → 裁切元素 → 这里聚合所有素材做分镜头编排</span>
|
||||
</div>
|
||||
)}
|
||||
</NodeShell>
|
||||
|
||||
@@ -301,16 +301,9 @@ export async function deleteGeneratedImage(jobId: string, frameIdx: number, genI
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export async function cutoutElement(
|
||||
jobId: string,
|
||||
frameIdx: number,
|
||||
elementId: string,
|
||||
background: "white" | "black" = "white",
|
||||
): Promise<Job> {
|
||||
export async function cutoutElement(jobId: string, frameIdx: number, elementId: string): Promise<Job> {
|
||||
const res = await fetch(`${API_BASE}/jobs/${jobId}/frames/${frameIdx}/elements/${elementId}/cutout`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ background }),
|
||||
})
|
||||
if (!res.ok) {
|
||||
const txt = await res.text().catch(() => "")
|
||||
|
||||
Reference in New Issue
Block a user