fix: surface resilient subject asset generation
This commit is contained in:
@@ -38,6 +38,7 @@ import {
|
||||
generateProductAngleAsset,
|
||||
generateSubjectAssets,
|
||||
generatedImageUrl,
|
||||
getJob,
|
||||
getRuntimeHealth,
|
||||
hasCutout,
|
||||
listCharacterLibrary,
|
||||
@@ -570,9 +571,9 @@ function audioModelTrace(models?: RuntimeModels): ModelTraceSpec {
|
||||
title: "音频解析",
|
||||
model: modelList([models?.asr, models?.translate, models?.asr_fallback]),
|
||||
chain: [
|
||||
`ASR 转写:优先 ${modelValue(models?.asr)},失败后尝试本机 ${modelValue(models?.local_asr)},再回退 ${modelValue(models?.asr_fallback)}`,
|
||||
`字幕翻译:${modelValue(models?.translate)} 输出中文逐句时间轴`,
|
||||
`讲话人 / 节奏 / 背景音:${modelValue(models?.asr_fallback)} 读取 audio.wav 做多模态音频分析`,
|
||||
`ASR 转写:优先 ${modelValue(models?.asr)};失败后尝试本机 ${modelValue(models?.local_asr)};仍失败才回退 ${modelValue(models?.asr_fallback)},并拒绝假字幕/重复时间轴`,
|
||||
`字幕翻译:${modelValue(models?.translate)} 按 ASR 段落输出中文;失败时保留原文时间轴,中文可为空`,
|
||||
`讲话人 / 节奏 / 背景音:${modelValue(models?.asr_fallback)} 读取 audio.wav + 转写时间轴做多模态分析;失败时用本地时长/段落估算兜底`,
|
||||
],
|
||||
note: "点击“解析音频”后触发;开始任务下载完成后也会自动走这条链路。",
|
||||
}
|
||||
@@ -583,9 +584,10 @@ function productModelTrace(models?: RuntimeModels): ModelTraceSpec {
|
||||
title: "产品视角识别 / 补图",
|
||||
model: modelList([models?.product_view, models?.image]),
|
||||
chain: [
|
||||
`批量视角识别:${modelValue(models?.product_view)} 一次读取同一产品多张图,标注视角、左右、上下、用途和风险`,
|
||||
`缺角度补图:${imageModelChain(models)} 读取最相关的多张已上传参考图,按同一肩颈按摩仪结构补齐缺失视角`,
|
||||
"前端只保存标注和 AI 补图结果;后续生成视频时每条最多挑 6 张相关产品图",
|
||||
`批量视角识别:${modelValue(models?.product_view)} 多图读取同一产品素材,标注视角、佩戴者左右、上下、内外侧、用途和风险`,
|
||||
"识别兜底:批量失败会按单图重试;仍失败或文件缺失时写入本地默认视角,并在 risk/note 标明兜底原因",
|
||||
`缺角度补图:${imageModelChain(models)} 走 /images/edits,最多读取 6 张已上传参考图补齐缺失视角;失败保留重试入口,不自动换模型`,
|
||||
"前端只保存标注和 AI 补图结果;后续首尾帧/视频规划每条最多挑 6 张相关产品图",
|
||||
],
|
||||
note: "上传产品图、重新识别、缺视角重试都会使用这组模型链路。",
|
||||
}
|
||||
@@ -594,11 +596,11 @@ function productModelTrace(models?: RuntimeModels): ModelTraceSpec {
|
||||
function similarSubjectModelTrace(models: RuntimeModels | undefined, subjectStyle: SubjectStyleMode): ModelTraceSpec {
|
||||
return {
|
||||
title: subjectStyle === "transparent_human" ? "相似透明骨架主体" : "相似普通真人主体",
|
||||
model: subjectImageModelChain(models),
|
||||
model: modelList([models?.vision, models?.subject_image]),
|
||||
chain: [
|
||||
"参考策略:先用视觉模型把关键帧/模板转成非身份化文字 brief,生图请求不再上传参考图",
|
||||
`视觉 brief:${modelValue(models?.vision)} 把关键帧/模板图转成非身份化文字 brief;失败时继续用用户方向和模板文字`,
|
||||
`主体类型:${subjectStyle === "transparent_human" ? "透明/半透明皮肤包裹可见白色骨架" : "普通商业广告真人"}`,
|
||||
`图像生成:${subjectImageModelChain(models)} 走 /images/generations 逐张生成高清图,视图数量由“全部/常用/自定义”决定`,
|
||||
`图像生成:${subjectImageModelChain(models)} 走 /images/generations 逐张文字生图;当前 similar 模式不上传原帧或模板图作为 image-edit 参考`,
|
||||
"身份锁定:整套图必须是同一个主体,性别表现、年龄段、体型、材质和风格保持一致",
|
||||
],
|
||||
note: "这是生成类似但创新的主体,不是复制、抠出或复刻源视频人物身份;内置形象也只作为方向参考。",
|
||||
@@ -611,8 +613,8 @@ function scriptRewriteModelTrace(models?: RuntimeModels): ModelTraceSpec {
|
||||
model: modelList([models?.audio_rewrite, models?.asr_fallback, models?.translate]),
|
||||
chain: [
|
||||
`主改写:${modelValue(models?.audio_rewrite)} 根据原文案、当前分镜、作者想法生成新口播`,
|
||||
`失败回退:依次尝试 ${modelValue(models?.asr_fallback)} 和 ${modelValue(models?.translate)}`,
|
||||
"返回结果只写入当前分镜文案编辑框;生成视频时再把当前文案写入分镜 action",
|
||||
`模型回退:依次尝试 ${modelValue(models?.asr_fallback)} 和 ${modelValue(models?.translate)};全部失败时用本地模板保留分镜可编辑`,
|
||||
"返回结果只写入当前分镜文案编辑框;点击保存规划后才写入 frame.storyboard.action",
|
||||
],
|
||||
}
|
||||
}
|
||||
@@ -625,8 +627,9 @@ function videoModelTrace(models: RuntimeModels | undefined, model: string): Mode
|
||||
`前端选择:${model}`,
|
||||
`后端解析:${resolveVideoModelLabel(models, model)}`,
|
||||
`服务商:${modelValue(models?.video_provider)} · ${modelValue(models?.video_base_url)}`,
|
||||
"输入:已确认的首尾帧、当前分镜文案、产品素材、相似主体资产和画面规划",
|
||||
"输出:异步候选视频,完成后回填到对应分镜行",
|
||||
"当前主工作台暂停直接提交视频;旧入口误触也会被页面层保护",
|
||||
"开放后输入会包含已确认首尾帧、当前分镜文案、产品素材、相似主体资产和画面规划",
|
||||
"输出为异步候选视频,完成后回填到对应分镜行;Sora 已停用",
|
||||
],
|
||||
}
|
||||
}
|
||||
@@ -2144,7 +2147,7 @@ function SourceReferenceBuildPanel({
|
||||
onJobUpdate: (job: Job) => void
|
||||
runtimeModels?: RuntimeModels
|
||||
}) {
|
||||
const [subjectBusy, setSubjectBusy] = useState(false)
|
||||
const [subjectBusyFor, setSubjectBusyFor] = useState<{ jobId: string; jobLabel: string; viewCount: number } | null>(null)
|
||||
const [subjectAssetBusy, setSubjectAssetBusy] = useState<string | null>(null)
|
||||
const [subjectMode, setSubjectMode] = useState<SubjectMode>("source_similar")
|
||||
const [subjectStyle, setSubjectStyle] = useState<SubjectStyleMode>("transparent_human")
|
||||
@@ -2220,6 +2223,7 @@ function SourceReferenceBuildPanel({
|
||||
? `${selectedCharacter.name} · 模板参考`
|
||||
: "源视频关键帧 · 相似创新"
|
||||
const templateRequired = subjectMode === "template" && !selectedSubjectTemplate && !selectedCharacter
|
||||
const subjectBusy = !!subjectBusyFor
|
||||
const generationCtaLabel = subjectMode === "template"
|
||||
? `用模板生成 ${selectedSubjectViews.length} 张主体视图`
|
||||
: `从源视频创新生成 ${selectedSubjectViews.length} 张主体视图`
|
||||
@@ -2265,13 +2269,14 @@ function SourceReferenceBuildPanel({
|
||||
}
|
||||
const baseFrame = subjectReferenceFrames[0]
|
||||
if (!baseFrame) return
|
||||
setSubjectBusy(true)
|
||||
const requestJobId = job.id
|
||||
setSubjectBusyFor({ jobId: requestJobId, jobLabel: shortId(requestJobId), viewCount: selectedSubjectViews.length })
|
||||
try {
|
||||
let workingJob = job
|
||||
let workingFrame = workingJob.frames.find((frame) => frame.index === baseFrame.index) ?? baseFrame
|
||||
let element = workingFrame.elements?.find(isSimilarActorElement)
|
||||
if (!element) {
|
||||
workingJob = await addElement(job.id, baseFrame.index, {
|
||||
workingJob = await addElement(requestJobId, baseFrame.index, {
|
||||
name_zh: selectedTemplatePrompt
|
||||
? `相似透明骨架主体 · ${selectedTemplatePrompt.name}`
|
||||
: subjectStyle === "transparent_human" ? "相似透明骨架主体" : "相似广告主角",
|
||||
@@ -2288,7 +2293,7 @@ function SourceReferenceBuildPanel({
|
||||
}
|
||||
if (!element) throw new Error("similar subject element missing")
|
||||
|
||||
const updated = await generateSubjectAssets(job.id, baseFrame.index, element.id, {
|
||||
const updated = await generateSubjectAssets(requestJobId, baseFrame.index, element.id, {
|
||||
subject_kind: "living",
|
||||
subject_style: subjectStyle,
|
||||
reconstruction_mode: "similar",
|
||||
@@ -2304,9 +2309,12 @@ function SourceReferenceBuildPanel({
|
||||
onJobUpdate(updated)
|
||||
toast.success(`相似主体 ${selectedSubjectViews.length} 张高清白底图已生成`)
|
||||
} catch (e) {
|
||||
try {
|
||||
onJobUpdate(await getJob(requestJobId))
|
||||
} catch { /* keep original error visible */ }
|
||||
toast.error("相似主体重构失败:" + (e instanceof Error ? e.message : String(e)))
|
||||
} finally {
|
||||
setSubjectBusy(false)
|
||||
setSubjectBusyFor(null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2438,7 +2446,8 @@ function SourceReferenceBuildPanel({
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className={`transition ${subjectMode === "source_similar" ? "pointer-events-none opacity-38 grayscale" : ""}`}>
|
||||
{subjectMode === "template" ? (
|
||||
<div>
|
||||
<div className="grid grid-cols-[repeat(auto-fill,minmax(120px,1fr))] gap-2">
|
||||
{subjectTemplateLibrary.map((template) => {
|
||||
const preview = characterPreviewImage(template)
|
||||
@@ -2494,6 +2503,11 @@ function SourceReferenceBuildPanel({
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-md border border-cyan-200/18 bg-cyan-300/[0.06] px-2.5 py-2 text-[10px] leading-snug text-cyan-50/62">
|
||||
当前跳过模板库:本次只用源视频关键帧的文字化主体特征生成创新主体。模板卡已收起,避免占用生成结果区域。
|
||||
</div>
|
||||
)}
|
||||
|
||||
{subjectMode === "template" && (selectedSubjectTemplate?.images?.length || selectedCharacter?.images?.length) ? (
|
||||
<div className="mt-2 flex gap-1.5 overflow-x-auto pb-0.5">
|
||||
@@ -2545,6 +2559,45 @@ function SourceReferenceBuildPanel({
|
||||
</div>
|
||||
<ModelTrace trace={similarSubjectModelTrace(runtimeModels, subjectStyle)} compact />
|
||||
</div>
|
||||
{subjectBusyFor ? (
|
||||
<div className="mb-2 rounded-md border border-cyan-200/20 bg-cyan-300/[0.07] px-2.5 py-2 text-[10px] leading-snug text-cyan-50/70">
|
||||
正在为素材 {subjectBusyFor.jobLabel} 生成 {subjectBusyFor.viewCount} 张主体视图;本次请求已锁定素材、参考帧、模式和视图数量,切换其他模块不会改变生成目标,完成后会回写到该素材。
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{visibleActorAssets.length ? (
|
||||
<div className="mb-2 grid grid-cols-[repeat(auto-fill,minmax(96px,1fr))] gap-2">
|
||||
{visibleActorAssets.map((asset) => {
|
||||
const busyMode = subjectAssetBusy?.endsWith(asset.id) ? subjectAssetBusy.split(":")[0] : ""
|
||||
return (
|
||||
<MediaAssetTile
|
||||
key={asset.id}
|
||||
src={subjectAssetUrl(job, asset)}
|
||||
href={subjectAssetUrl(job, asset)}
|
||||
alt={asset.label || asset.view}
|
||||
label={asset.label || asset.view || "主体视图预览"}
|
||||
meta={asset.width && asset.height ? `${asset.width}x${asset.height}` : undefined}
|
||||
className="aspect-[9/16] w-20 bg-white 2xl:w-24"
|
||||
objectFit="contain"
|
||||
title={asset.label || asset.view}
|
||||
actions={[{
|
||||
key: "regen",
|
||||
label: "重新生成这一张",
|
||||
icon: <RefreshCw className="h-3 w-3" />,
|
||||
tone: "cyan",
|
||||
busy: busyMode === "regen",
|
||||
disabled: !!subjectAssetBusy,
|
||||
onClick: () => void regenerateSubjectAsset(asset),
|
||||
}]}
|
||||
onDelete={() => void deleteActorAsset(asset)}
|
||||
deleting={busyMode === "delete"}
|
||||
deleteDisabled={!!subjectAssetBusy}
|
||||
deleteLabel="删除这一张"
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="grid gap-2 xl:grid-cols-[auto_auto_minmax(220px,1fr)_auto] xl:items-start">
|
||||
<div className="flex rounded-md border border-white/10 bg-black/28 p-0.5">
|
||||
@@ -2595,7 +2648,7 @@ function SourceReferenceBuildPanel({
|
||||
className="inline-flex h-9 min-w-[170px] items-center justify-center gap-1 rounded-md bg-white px-3 text-[11px] 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" />}
|
||||
{generationCtaLabel}
|
||||
{subjectBusyFor ? `生成中 · ${subjectBusyFor.jobLabel}` : generationCtaLabel}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -2623,45 +2676,13 @@ function SourceReferenceBuildPanel({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{visibleActorAssets.length ? (
|
||||
<div className="mt-2 grid grid-cols-[repeat(auto-fill,minmax(96px,1fr))] gap-2">
|
||||
{visibleActorAssets.map((asset) => {
|
||||
const busyMode = subjectAssetBusy?.endsWith(asset.id) ? subjectAssetBusy.split(":")[0] : ""
|
||||
return (
|
||||
<MediaAssetTile
|
||||
key={asset.id}
|
||||
src={subjectAssetUrl(job, asset)}
|
||||
href={subjectAssetUrl(job, asset)}
|
||||
alt={asset.label || asset.view}
|
||||
label={asset.label || asset.view || "主体视图预览"}
|
||||
meta={asset.width && asset.height ? `${asset.width}x${asset.height}` : undefined}
|
||||
className="aspect-[9/16] w-20 bg-white 2xl:w-24"
|
||||
objectFit="contain"
|
||||
title={asset.label || asset.view}
|
||||
actions={[{
|
||||
key: "regen",
|
||||
label: "重新生成这一张",
|
||||
icon: <RefreshCw className="h-3 w-3" />,
|
||||
tone: "cyan",
|
||||
busy: busyMode === "regen",
|
||||
disabled: !!subjectAssetBusy,
|
||||
onClick: () => void regenerateSubjectAsset(asset),
|
||||
}]}
|
||||
onDelete={() => void deleteActorAsset(asset)}
|
||||
deleting={busyMode === "delete"}
|
||||
deleteDisabled={!!subjectAssetBusy}
|
||||
deleteLabel="删除这一张"
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
{!visibleActorAssets.length ? (
|
||||
<div className="mt-2 rounded border border-dashed border-white/12 px-2 py-2 text-[10.5px] leading-snug text-white/32">
|
||||
{subjectMode === "template"
|
||||
? "先选主体模板,再生成新主体视图;模板只作为文字化创意方向,不再作为强参考图复制。"
|
||||
: "直接使用关键帧的文字化主体特征生成创新主体;后端不会上传源图给生图端点。"}
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -673,7 +673,7 @@ export const Dashboard = forwardRef<DashboardHandle, Props>(function Dashboard({
|
||||
{key === "videogen" && (
|
||||
<>
|
||||
<KanbanCard tone="violet" tags={["SKG 网关"]} title="Seedance / Kling / Veo 3">
|
||||
<div className="text-[11px] text-[var(--text-soft)]">通过 /v1/videos 网关提交,模型 ID 走环境变量映射</div>
|
||||
<div className="text-[11px] text-[var(--text-soft)]">按后端 VIDEO_CREATE_PATHS 提交,模型 ID 走环境变量映射</div>
|
||||
</KanbanCard>
|
||||
<KanbanCard tone="violet" tags={["外部"]} title="Seedance">
|
||||
<div className="text-[11px] text-[var(--text-soft)]">字节跳动 · 需独立 API key</div>
|
||||
|
||||
Reference in New Issue
Block a user