diff --git a/.memory/worklog.json b/.memory/worklog.json index 52d4d62..cec0720 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -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 } ] } diff --git a/web/components/ad-recreation-board.tsx b/web/components/ad-recreation-board.tsx index 6626b59..1acc1b6 100644 --- a/web/components/ad-recreation-board.tsx +++ b/web/components/ad-recreation-board.tsx @@ -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(null) const [framePreview, setFramePreview] = useState<{ index: number; left: number; top: number } | null>(null) + const [subjectStyle, setSubjectStyle] = useState("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({
{framePreviewPortal}
- } title="关键帧 / 相似主角" /> + } title="关键帧 / 相似主体" />
- {frames.length ? `${frames.length} 张` : "待抽帧"} · 已选 {selectedReferenceFrames.length} + {referenceCountLabel} + ))} +
+ 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" + /> {actorAssets.length}/6
) : (
- 选择能代表主角状态的关键帧后,用图像模型生成“类似但不复刻”的 6 张白底视图。 + 可直接用全部关键帧生成;勾选关键帧后会只用已选帧。六视图会围绕同一个统一主体生成。
)}