fix: isolate subject reference generation
This commit is contained in:
@@ -653,10 +653,14 @@ function emptySubjectPromptMemory(): Record<SubjectReconstructionMode, string[]>
|
||||
return { realistic: [], cartoon: [], elements: [], custom: [] }
|
||||
}
|
||||
|
||||
function loadSubjectPromptMemory(): Record<SubjectReconstructionMode, string[]> {
|
||||
function subjectScopedStorageKey(baseKey: string, jobId: string) {
|
||||
return `${baseKey}:${jobId}`
|
||||
}
|
||||
|
||||
function loadSubjectPromptMemory(jobId: string): Record<SubjectReconstructionMode, string[]> {
|
||||
if (typeof window === "undefined") return emptySubjectPromptMemory()
|
||||
try {
|
||||
const parsed = JSON.parse(window.localStorage.getItem(SUBJECT_PROMPT_MEMORY_KEY) || "{}") as Partial<Record<SubjectReconstructionMode, string[]>>
|
||||
const parsed = JSON.parse(window.localStorage.getItem(subjectScopedStorageKey(SUBJECT_PROMPT_MEMORY_KEY, jobId)) || "{}") as Partial<Record<SubjectReconstructionMode, string[]>>
|
||||
const next = emptySubjectPromptMemory()
|
||||
for (const mode of Object.keys(next) as SubjectReconstructionMode[]) {
|
||||
next[mode] = Array.isArray(parsed[mode]) ? parsed[mode]!.filter(Boolean).slice(0, SUBJECT_PROMPT_MEMORY_LIMIT) : []
|
||||
@@ -667,25 +671,25 @@ function loadSubjectPromptMemory(): Record<SubjectReconstructionMode, string[]>
|
||||
}
|
||||
}
|
||||
|
||||
function saveSubjectPromptMemory(memory: Record<SubjectReconstructionMode, string[]>) {
|
||||
function saveSubjectPromptMemory(jobId: string, memory: Record<SubjectReconstructionMode, string[]>) {
|
||||
if (typeof window === "undefined") return
|
||||
try {
|
||||
window.localStorage.setItem(SUBJECT_PROMPT_MEMORY_KEY, JSON.stringify(memory))
|
||||
window.localStorage.setItem(subjectScopedStorageKey(SUBJECT_PROMPT_MEMORY_KEY, jobId), JSON.stringify(memory))
|
||||
} catch {
|
||||
/* localStorage may be unavailable */
|
||||
}
|
||||
}
|
||||
|
||||
function loadSubjectImageModelPreference(): SubjectImageModelPreference {
|
||||
function loadSubjectImageModelPreference(jobId: string): SubjectImageModelPreference {
|
||||
if (typeof window === "undefined") return "auto"
|
||||
const raw = window.localStorage.getItem(SUBJECT_MODEL_MEMORY_KEY)
|
||||
const raw = window.localStorage.getItem(subjectScopedStorageKey(SUBJECT_MODEL_MEMORY_KEY, jobId))
|
||||
return SUBJECT_IMAGE_MODEL_OPTIONS.some((item) => item.value === raw) ? raw as SubjectImageModelPreference : "auto"
|
||||
}
|
||||
|
||||
function saveSubjectImageModelPreference(value: SubjectImageModelPreference) {
|
||||
function saveSubjectImageModelPreference(jobId: string, value: SubjectImageModelPreference) {
|
||||
if (typeof window === "undefined") return
|
||||
try {
|
||||
window.localStorage.setItem(SUBJECT_MODEL_MEMORY_KEY, value)
|
||||
window.localStorage.setItem(subjectScopedStorageKey(SUBJECT_MODEL_MEMORY_KEY, jobId), value)
|
||||
} catch {
|
||||
/* localStorage may be unavailable */
|
||||
}
|
||||
@@ -1216,6 +1220,24 @@ function buildSimilarSubjectPrompt(
|
||||
return base.join(" ")
|
||||
}
|
||||
|
||||
function buildSourceLockedSubjectPrompt(subjectStyle: SubjectStyleMode) {
|
||||
const base = [
|
||||
"Source-locked subject replication from the selected reference frames.",
|
||||
"Use the attached reference frame(s) as the primary visual source for the same visible subject: preserve gender presentation, regional/ethnic appearance category, skin-tone family, body proportions, hair length/color/silhouette, face structure impression, wardrobe category, outfit colors, fit, and commercial role as closely as the model allows.",
|
||||
"Generate separate clean white-background multi-view assets of that same source subject, removing only source video background, platform UI, captions, watermarks, compression artifacts, and accidental occlusions.",
|
||||
"Do not invent a different actor, different ethnicity, different gender, different body type, different hair design, or different outfit when the reference evidence is visible.",
|
||||
"If multiple frames are supplied, treat them as evidence for one same subject and build one locked subject bible before rendering every view.",
|
||||
"Keep the neck, collarbone, shoulders, upper back, and side neck clean and usable for SKG neck-and-shoulder product placement.",
|
||||
]
|
||||
if (subjectStyle === "cartoon_subject") {
|
||||
base.push("If a cartoon style is requested, convert the same visible source subject into one consistent stylized character while preserving the reference's main appearance and outfit cues.")
|
||||
} else {
|
||||
base.push("The subject must remain a believable normal commercial ad actor, not a transparent or skeleton character.")
|
||||
}
|
||||
base.push("Output high-definition assets; each image is one requested view of the same unified subject.")
|
||||
return base.join(" ")
|
||||
}
|
||||
|
||||
function subjectAssetUrl(job: Job, asset: SubjectAsset) {
|
||||
if (!asset.url && asset.status && asset.status !== "completed") return ""
|
||||
return apiAssetUrl(asset.url) || resolveImageRefUrl(job.id, { kind: "asset", frame_idx: 0, element_id: asset.id })
|
||||
@@ -3287,8 +3309,8 @@ function SourceSubjectPipeline({
|
||||
const [activeDropMode, setActiveDropMode] = useState<SubjectReconstructionMode | null>(null)
|
||||
const [conversionFrameIndicesByMode, setConversionFrameIndicesByMode] = useState<Record<SubjectReconstructionMode, number[]>>(() => ({ ...EMPTY_RECONSTRUCTION_FRAME_MAP }))
|
||||
const [reconstructionDirections, setReconstructionDirections] = useState<Record<SubjectReconstructionMode, string>>(() => ({ ...DEFAULT_RECONSTRUCTION_DIRECTIONS }))
|
||||
const [subjectImageModelPreference, setSubjectImageModelPreference] = useState<SubjectImageModelPreference>(() => loadSubjectImageModelPreference())
|
||||
const [promptMemoryByMode, setPromptMemoryByMode] = useState<Record<SubjectReconstructionMode, string[]>>(() => loadSubjectPromptMemory())
|
||||
const [subjectImageModelPreference, setSubjectImageModelPreference] = useState<SubjectImageModelPreference>(() => loadSubjectImageModelPreference(job.id))
|
||||
const [promptMemoryByMode, setPromptMemoryByMode] = useState<Record<SubjectReconstructionMode, string[]>>(() => loadSubjectPromptMemory(job.id))
|
||||
const [cartoonStyle, setCartoonStyle] = useState<CartoonReconstructionStyle>("3d_animation")
|
||||
const [cartoonStyleOpen, setCartoonStyleOpen] = useState(false)
|
||||
const [subjectBusyFor, setSubjectBusyFor] = useState<{ jobId: string; jobLabel: string; mode: SubjectReconstructionMode; viewCount: number; sourceCount: number; profileLabel: string; modelLabel: string } | null>(null)
|
||||
@@ -3383,6 +3405,8 @@ function SourceSubjectPipeline({
|
||||
useEffect(() => {
|
||||
setConversionFrameIndicesByMode({ ...EMPTY_RECONSTRUCTION_FRAME_MAP })
|
||||
setReconstructionDirections({ ...DEFAULT_RECONSTRUCTION_DIRECTIONS })
|
||||
setSubjectImageModelPreference(loadSubjectImageModelPreference(job.id))
|
||||
setPromptMemoryByMode(loadSubjectPromptMemory(job.id))
|
||||
setLastSubjectProfile(null)
|
||||
setSubjectBusyFor(null)
|
||||
setSubjectAssetBusy(null)
|
||||
@@ -3392,12 +3416,12 @@ function SourceSubjectPipeline({
|
||||
}, [job.id])
|
||||
|
||||
useEffect(() => {
|
||||
saveSubjectImageModelPreference(subjectImageModelPreference)
|
||||
}, [subjectImageModelPreference])
|
||||
saveSubjectImageModelPreference(job.id, subjectImageModelPreference)
|
||||
}, [job.id, subjectImageModelPreference])
|
||||
|
||||
useEffect(() => {
|
||||
saveSubjectPromptMemory(promptMemoryByMode)
|
||||
}, [promptMemoryByMode])
|
||||
saveSubjectPromptMemory(job.id, promptMemoryByMode)
|
||||
}, [job.id, promptMemoryByMode])
|
||||
|
||||
useEffect(() => {
|
||||
if (expandedSubjectPackKey && !subjectAssetPacks.some((pack) => pack.key === expandedSubjectPackKey)) {
|
||||
@@ -3455,17 +3479,23 @@ function SourceSubjectPipeline({
|
||||
const sourceFrames = sourceIndices
|
||||
.map((index) => frames.find((frame) => frame.index === index))
|
||||
.filter((frame): frame is KeyFrame => !!frame)
|
||||
const rawDirection = reconstructionDirections[mode].trim()
|
||||
const sourceLockedReplication = mode === "custom" && !rawDirection
|
||||
if (!sourceFrames.length && mode !== "custom") {
|
||||
toast.warning(`先把参考帧拖到${reconstructionModeConfig(mode).label}。`)
|
||||
return
|
||||
}
|
||||
if (!sourceFrames.length && sourceLockedReplication) {
|
||||
toast.warning("自主描述没有文字时,需要先拖入参考帧用于形象复刻。")
|
||||
return
|
||||
}
|
||||
const baseFrame = sourceFrames[0] ?? frames[0]
|
||||
if (!baseFrame) {
|
||||
toast.warning("先完成抽帧,或从胶片加入至少一张参考帧。")
|
||||
return
|
||||
}
|
||||
const requestJobId = job.id
|
||||
const requestProfile = mode === "custom" && reconstructionDirections.custom.trim()
|
||||
const requestProfile = sourceLockedReplication || (mode === "custom" && rawDirection)
|
||||
? null
|
||||
: buildSubjectProfileForRequest()
|
||||
const subjectStyle = reconstructionSubjectStyle(mode)
|
||||
@@ -3502,13 +3532,15 @@ function SourceSubjectPipeline({
|
||||
const updated = await generateSubjectAssets(requestJobId, baseFrame.index, element.id, {
|
||||
subject_kind: "living",
|
||||
subject_style: subjectStyle,
|
||||
reconstruction_mode: "similar",
|
||||
reconstruction_mode: sourceLockedReplication ? "same" : "similar",
|
||||
background: "white",
|
||||
size: SUBJECT_ASSET_SIZE,
|
||||
source_frame_indices: sourceFrames.slice(0, RECONSTRUCTION_FRAME_LIMIT).map((frame) => frame.index),
|
||||
views: selectedSubjectViews,
|
||||
subject_profile: requestProfile?.payload ?? null,
|
||||
prompt: buildSimilarSubjectPrompt(subjectStyle, userDirection, null, requestProfile),
|
||||
prompt: sourceLockedReplication
|
||||
? buildSourceLockedSubjectPrompt(subjectStyle)
|
||||
: buildSimilarSubjectPrompt(subjectStyle, userDirection, null, requestProfile),
|
||||
image_model_preference: subjectImageModelPreference,
|
||||
replace_views: false,
|
||||
pack_label: `${reconstructionModeConfig(mode).label} ${new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit", hour12: false })}`,
|
||||
@@ -3563,13 +3595,19 @@ function SourceSubjectPipeline({
|
||||
const sourceIndices = asset.source_frame_indices?.length
|
||||
? asset.source_frame_indices
|
||||
: conversionFrameIndicesByMode[mode]
|
||||
const rawDirection = reconstructionDirections[mode].trim()
|
||||
const sourceLockedReplication = mode === "custom" && !rawDirection
|
||||
if (!sourceIndices.length && mode !== "custom") {
|
||||
toast.warning("转换层没有参考帧,不能重生。")
|
||||
return
|
||||
}
|
||||
if (!sourceIndices.length && sourceLockedReplication) {
|
||||
toast.warning("自主描述没有文字时,需要参考帧才能复刻重生。")
|
||||
return
|
||||
}
|
||||
setSubjectAssetBusy(`regen:${asset.id}`)
|
||||
try {
|
||||
const requestProfile = mode === "custom" && reconstructionDirections.custom.trim()
|
||||
const requestProfile = sourceLockedReplication || (mode === "custom" && rawDirection)
|
||||
? null
|
||||
: lastSubjectProfile ?? buildSubjectProfileForRequest()
|
||||
const subjectStyle = reconstructionSubjectStyle(mode)
|
||||
@@ -3577,18 +3615,20 @@ function SourceSubjectPipeline({
|
||||
const updated = await generateSubjectAssets(job.id, frame.index, element.id, {
|
||||
subject_kind: "living",
|
||||
subject_style: subjectStyle,
|
||||
reconstruction_mode: "similar",
|
||||
reconstruction_mode: sourceLockedReplication ? "same" : "similar",
|
||||
background: asset.background || "white",
|
||||
size: SUBJECT_ASSET_SIZE,
|
||||
source_frame_indices: sourceIndices.slice(0, RECONSTRUCTION_FRAME_LIMIT),
|
||||
views: [asset.view],
|
||||
subject_profile: requestProfile?.payload ?? null,
|
||||
prompt: buildSimilarSubjectPrompt(
|
||||
subjectStyle,
|
||||
buildReconstructionDirection(mode, reconstructionDirections[mode], cartoonStyle),
|
||||
null,
|
||||
requestProfile,
|
||||
),
|
||||
prompt: sourceLockedReplication
|
||||
? buildSourceLockedSubjectPrompt(subjectStyle)
|
||||
: buildSimilarSubjectPrompt(
|
||||
subjectStyle,
|
||||
buildReconstructionDirection(mode, reconstructionDirections[mode], cartoonStyle),
|
||||
null,
|
||||
requestProfile,
|
||||
),
|
||||
image_model_preference: subjectImageModelPreference,
|
||||
replace_views: true,
|
||||
pack_id: asset.pack_id ?? "",
|
||||
|
||||
Reference in New Issue
Block a user