auto-save 2026-05-17 23:19 (~2)
This commit is contained in:
@@ -1,31 +1,5 @@
|
||||
{
|
||||
"entries": [
|
||||
{
|
||||
"files_changed": 1,
|
||||
"hash": "00699ba",
|
||||
"message": "auto-save 2026-05-15 14:22 (~1)",
|
||||
"ts": "2026-05-15T14:22:44+08:00",
|
||||
"type": "commit"
|
||||
},
|
||||
{
|
||||
"files_changed": 1,
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 1 项未提交变更 · 最近提交:auto-save 2026-05-15 14:22 (~1)",
|
||||
"ts": "2026-05-15T06:24:46Z",
|
||||
"type": "session-heartbeat"
|
||||
},
|
||||
{
|
||||
"files_changed": 1,
|
||||
"hash": "985b76a",
|
||||
"message": "auto-save 2026-05-15 14:28 (~1)",
|
||||
"ts": "2026-05-15T14:28:16+08:00",
|
||||
"type": "commit"
|
||||
},
|
||||
{
|
||||
"files_changed": 1,
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 1 项未提交变更 · 最近提交:auto-save 2026-05-15 14:28 (~1)",
|
||||
"ts": "2026-05-15T06:34:46Z",
|
||||
"type": "session-heartbeat"
|
||||
},
|
||||
{
|
||||
"files_changed": 1,
|
||||
"hash": "8090674",
|
||||
@@ -3259,6 +3233,32 @@
|
||||
"type": "session-heartbeat",
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 2 项未提交变更 · 最近提交:auto-save 2026-05-17 22:57 (~2)",
|
||||
"files_changed": 2
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-17T23:03:08+08:00",
|
||||
"type": "commit",
|
||||
"message": "auto-save 2026-05-17 23:03 (~3)",
|
||||
"hash": "290a833",
|
||||
"files_changed": 3
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-17T23:06:31+08:00",
|
||||
"type": "commit",
|
||||
"message": "fix: clarify source frame workflow copy",
|
||||
"hash": "a1de7f2",
|
||||
"files_changed": 1
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-17T15:08:31Z",
|
||||
"type": "session-heartbeat",
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:fix: clarify source frame workflow copy",
|
||||
"files_changed": 1
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-17T15:18:31Z",
|
||||
"type": "session-heartbeat",
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 2 项未提交变更 · 最近提交:fix: clarify source frame workflow copy",
|
||||
"files_changed": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -93,6 +93,7 @@ type AudioStoryboardRow = {
|
||||
}
|
||||
|
||||
type ProductRefItem = ProductRefStateItem
|
||||
type SubjectStyleMode = "transparent_human" | "source_actor"
|
||||
|
||||
const PRODUCT_VIEW_SLOTS = [
|
||||
{ value: "front", label: "正面/外侧", hint: "整体 U 形轮廓、开口宽度、外壳主外观" },
|
||||
@@ -251,7 +252,33 @@ function closestFrameForTime(frames: KeyFrame[], time: number) {
|
||||
|
||||
function isSimilarActorElement(element: KeyElement) {
|
||||
const name = `${element.name_zh || ""} ${element.name_en || ""}`.toLowerCase()
|
||||
return name.includes("相似主角") || name.includes("similar ad actor") || name.includes("similar actor")
|
||||
return name.includes("相似主角") || name.includes("相似主体") || name.includes("similar ad actor") || name.includes("similar actor") || name.includes("similar subject")
|
||||
}
|
||||
|
||||
function buildSimilarSubjectPrompt(subjectStyle: SubjectStyleMode, direction: string) {
|
||||
const base = [
|
||||
"Create a new similar but non-identical information-feed ad subject from the selected reference frames.",
|
||||
"Treat all selected frames as evidence for ONE same subject, not multiple different subjects.",
|
||||
"Lock one consistent character bible before generating: same gender presentation, age range, body proportions, head shape, material, silhouette, commercial style, and visual identity across all six views.",
|
||||
"If the user direction asks to change gender, age, or style, apply that single change uniformly to every view; never mix male/female, young/old, or multiple style identities inside one six-view set.",
|
||||
"Keep the pose vocabulary, camera-readability, creator-ad energy, and commercial clarity, but do not copy the exact source identity, face, watermark, captions, platform UI, or pixels.",
|
||||
]
|
||||
if (subjectStyle === "transparent_human") {
|
||||
base.push(
|
||||
"The subject must be a transparent humanoid: transparent or translucent skin/body shell wrapping a clean visible white skeleton inside the same body.",
|
||||
"Keep transparent skin, visible spine, rib cage, pelvis, arm bones, leg bones, and a friendly non-horror wellness advertising look consistent in every view.",
|
||||
"Do not generate a normal opaque human, skeleton-only character, medical anatomy, organs, blood, gore, surgery, hospital, or horror imagery.",
|
||||
)
|
||||
} else {
|
||||
base.push(
|
||||
"The subject must be a normal believable commercial ad actor, not a transparent or skeleton character.",
|
||||
"Keep wardrobe category, age range, gender presentation, body proportion, and creator-ad styling consistent in every view.",
|
||||
)
|
||||
}
|
||||
const trimmed = direction.trim()
|
||||
if (trimmed) base.push(`User unified subject direction: ${trimmed}`)
|
||||
base.push("Output separate pure white background six-view assets; each image is one view of the same unified subject.")
|
||||
return base.join(" ")
|
||||
}
|
||||
|
||||
function subjectAssetUrl(job: Job, asset: SubjectAsset) {
|
||||
@@ -1255,13 +1282,19 @@ function SourceReferenceBuildPanel({
|
||||
const [subjectBusy, setSubjectBusy] = useState(false)
|
||||
const [deletingFrame, setDeletingFrame] = useState<number | null>(null)
|
||||
const [framePreview, setFramePreview] = useState<{ index: number; left: number; top: number } | null>(null)
|
||||
const [subjectStyle, setSubjectStyle] = useState<SubjectStyleMode>("transparent_human")
|
||||
const [subjectDirection, setSubjectDirection] = useState("")
|
||||
const frames = useMemo(() => [...job.frames].sort((a, b) => a.timestamp - b.timestamp), [job.frames])
|
||||
const selectedReferenceFrames = useMemo(
|
||||
() => frames.filter((frame) => selectedFrames.has(frame.index)),
|
||||
[frames, selectedFrames],
|
||||
)
|
||||
const subjectReferenceFrames = useMemo(
|
||||
() => selectedReferenceFrames.length ? selectedReferenceFrames : frames,
|
||||
[frames, selectedReferenceFrames],
|
||||
)
|
||||
const actorSource = useMemo(() => {
|
||||
const pool = selectedReferenceFrames.length ? selectedReferenceFrames : frames
|
||||
const pool = subjectReferenceFrames
|
||||
for (const frame of pool) {
|
||||
const element = frame.elements?.find(isSimilarActorElement)
|
||||
if (element?.subject_assets?.length) return { frame, element }
|
||||
@@ -1271,9 +1304,14 @@ function SourceReferenceBuildPanel({
|
||||
if (element) return { frame, element }
|
||||
}
|
||||
return null
|
||||
}, [frames, selectedReferenceFrames])
|
||||
}, [subjectReferenceFrames])
|
||||
const actorAssets = actorSource?.element.subject_assets ?? []
|
||||
const previewFrame = framePreview ? frames.find((frame) => frame.index === framePreview.index) ?? null : null
|
||||
const referenceCountLabel = selectedReferenceFrames.length
|
||||
? `使用已选 ${selectedReferenceFrames.length} 张`
|
||||
: frames.length
|
||||
? `默认使用全部 ${frames.length} 张`
|
||||
: "待抽帧"
|
||||
|
||||
const extractKeyframes = async () => {
|
||||
setExtracting(true)
|
||||
@@ -1292,11 +1330,11 @@ function SourceReferenceBuildPanel({
|
||||
}
|
||||
|
||||
const generateSimilarActor = async () => {
|
||||
if (!selectedReferenceFrames.length) {
|
||||
toast.warning("请先从 12 张关键帧里选择主角参考帧。")
|
||||
if (!frames.length) {
|
||||
toast.warning("请先自动抽帧 12 张,或在原版视频上手动补帧。")
|
||||
return
|
||||
}
|
||||
const baseFrame = selectedReferenceFrames[0]
|
||||
const baseFrame = subjectReferenceFrames[0]
|
||||
if (!baseFrame) return
|
||||
setSubjectBusy(true)
|
||||
try {
|
||||
@@ -1305,9 +1343,9 @@ function SourceReferenceBuildPanel({
|
||||
let element = workingFrame.elements?.find(isSimilarActorElement)
|
||||
if (!element) {
|
||||
workingJob = await addElement(job.id, baseFrame.index, {
|
||||
name_zh: "相似主角",
|
||||
name_en: "similar ad actor",
|
||||
position: "source-video main presenter selected from global keyframes",
|
||||
name_zh: subjectStyle === "transparent_human" ? "相似透明骨架主体" : "相似广告主角",
|
||||
name_en: subjectStyle === "transparent_human" ? "similar transparent skeleton humanoid subject" : "similar ad actor",
|
||||
position: "source-video main subject selected from global keyframes",
|
||||
source: "manual",
|
||||
})
|
||||
onJobUpdate(workingJob)
|
||||
@@ -1315,22 +1353,22 @@ function SourceReferenceBuildPanel({
|
||||
element = workingFrame.elements?.find(isSimilarActorElement)
|
||||
?? workingFrame.elements?.[workingFrame.elements.length - 1]
|
||||
}
|
||||
if (!element) throw new Error("similar actor element missing")
|
||||
if (!element) throw new Error("similar subject element missing")
|
||||
|
||||
const updated = await generateSubjectAssets(job.id, baseFrame.index, element.id, {
|
||||
subject_kind: "living",
|
||||
subject_style: "source_actor",
|
||||
subject_style: subjectStyle,
|
||||
reconstruction_mode: "similar",
|
||||
background: "white",
|
||||
size: "1024",
|
||||
source_frame_indices: selectedReferenceFrames.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"],
|
||||
prompt: "Create a new similar information-feed ad presenter, not a replica of the source person. Keep the creator-ad energy, pose vocabulary, wardrobe category, shot readability, and commercial realism. White background six-view reference sheet output, consistent actor identity across views.",
|
||||
prompt: buildSimilarSubjectPrompt(subjectStyle, subjectDirection),
|
||||
})
|
||||
onJobUpdate(updated)
|
||||
toast.success("相似主角 6 张白底视图已生成")
|
||||
toast.success("相似主体 6 张白底视图已生成")
|
||||
} catch (e) {
|
||||
toast.error("相似主角重构失败:" + (e instanceof Error ? e.message : String(e)))
|
||||
toast.error("相似主体重构失败:" + (e instanceof Error ? e.message : String(e)))
|
||||
} finally {
|
||||
setSubjectBusy(false)
|
||||
}
|
||||
@@ -1385,10 +1423,10 @@ function SourceReferenceBuildPanel({
|
||||
<div className="min-w-0">
|
||||
{framePreviewPortal}
|
||||
<div className="mb-2 flex items-center justify-between gap-3">
|
||||
<SectionTitle icon={<ImageIcon className="h-4 w-4" />} title="关键帧 / 相似主角" />
|
||||
<SectionTitle icon={<ImageIcon className="h-4 w-4" />} title="关键帧 / 相似主体" />
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="rounded-md border border-white/10 bg-black/35 px-2 py-1 text-[11px] text-white/45">
|
||||
{frames.length ? `${frames.length} 张` : "待抽帧"} · 已选 {selectedReferenceFrames.length}
|
||||
{referenceCountLabel}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
@@ -1405,7 +1443,7 @@ function SourceReferenceBuildPanel({
|
||||
<div className="h-[250px] overflow-y-auto rounded-md border border-white/10 bg-black/32 p-2 2xl:h-[290px]">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-[10.5px] text-white/34">缩略图完整显示,悬停看大图。</span>
|
||||
<span className="text-[10.5px] text-white/30">点击选择主角参考</span>
|
||||
<span className="text-[10.5px] text-white/30">不勾选则默认用全部帧</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 grid grid-cols-6 gap-1.5 md:grid-cols-8 xl:grid-cols-12 2xl:grid-cols-16">
|
||||
@@ -1459,14 +1497,37 @@ function SourceReferenceBuildPanel({
|
||||
</div>
|
||||
|
||||
<div className="mt-2 border-t border-white/8 pt-2">
|
||||
<div className="mb-1.5 flex items-center justify-between text-[10px] text-white/36">
|
||||
<span>相似主角白底视图</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="mb-1.5 flex flex-wrap items-center justify-between gap-2 text-[10px] text-white/36">
|
||||
<span>相似主体白底视图</span>
|
||||
<div className="flex min-w-0 flex-wrap items-center justify-end gap-2">
|
||||
<div className="flex rounded-md border border-white/10 bg-black/28 p-0.5">
|
||||
{[
|
||||
{ value: "transparent_human" as const, label: "透明骨架" },
|
||||
{ value: "source_actor" as const, label: "普通真人" },
|
||||
].map((item) => (
|
||||
<button
|
||||
key={item.value}
|
||||
type="button"
|
||||
onClick={() => setSubjectStyle(item.value)}
|
||||
className={`h-6 rounded px-2 text-[10px] font-semibold transition ${
|
||||
subjectStyle === item.value ? "bg-white text-black" : "text-white/45 hover:text-white"
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<input
|
||||
value={subjectDirection}
|
||||
onChange={(event) => setSubjectDirection(event.target.value)}
|
||||
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"
|
||||
/>
|
||||
<span>{actorAssets.length}/6</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void generateSimilarActor()}
|
||||
disabled={!selectedReferenceFrames.length || subjectBusy}
|
||||
disabled={!frames.length || subjectBusy}
|
||||
className="inline-flex h-7 items-center justify-center gap-1 rounded-md bg-white px-2 text-[10.5px] font-semibold text-black transition hover:bg-white/90 disabled:cursor-not-allowed disabled:opacity-40"
|
||||
>
|
||||
{subjectBusy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Sparkles className="h-3.5 w-3.5" />}
|
||||
@@ -1491,7 +1552,7 @@ function SourceReferenceBuildPanel({
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded border border-dashed border-white/12 px-2 py-2 text-[10.5px] leading-snug text-white/32">
|
||||
选择能代表主角状态的关键帧后,用图像模型生成“类似但不复刻”的 6 张白底视图。
|
||||
可直接用全部关键帧生成;勾选关键帧后会只用已选帧。六视图会围绕同一个统一主体生成。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user