feat: connect subject template library
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
||||
type GeneratedVideo,
|
||||
type ImageRef,
|
||||
type CharacterLibraryItem,
|
||||
type SubjectTemplateItem,
|
||||
type Job,
|
||||
type KeyElement,
|
||||
type KeyFrame,
|
||||
@@ -39,11 +40,14 @@ import {
|
||||
getRuntimeHealth,
|
||||
hasCutout,
|
||||
listCharacterLibrary,
|
||||
listSubjectTemplates,
|
||||
representativeCutoutUrl,
|
||||
resolveImageRefUrl,
|
||||
rewriteStoryboardScript,
|
||||
saveSubjectTemplate,
|
||||
saveProductRefs,
|
||||
sourceAudioUrl,
|
||||
subjectTemplateImageUrl,
|
||||
updateStoryboard,
|
||||
uploadStoryboardAsset,
|
||||
videoUrl,
|
||||
@@ -485,7 +489,9 @@ function findSimilarActorSource(preferredFrames: KeyFrame[], allFrames: KeyFrame
|
||||
return null
|
||||
}
|
||||
|
||||
function buildSimilarSubjectPrompt(subjectStyle: SubjectStyleMode, direction: string, selectedCharacter?: CharacterLibraryItem | null) {
|
||||
type SubjectTemplatePromptSource = { name: string; sourceLabel: string } | null
|
||||
|
||||
function buildSimilarSubjectPrompt(subjectStyle: SubjectStyleMode, direction: string, selectedTemplate?: SubjectTemplatePromptSource) {
|
||||
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.",
|
||||
@@ -495,10 +501,10 @@ function buildSimilarSubjectPrompt(subjectStyle: SubjectStyleMode, direction: st
|
||||
"This is for SKG neck-and-shoulder wearable massage device videos: keep neck, collarbone, shoulders, side neck, upper back, shoulder blades, and product placement area clean and visible.",
|
||||
"Output high-definition assets suitable for downstream video generation.",
|
||||
]
|
||||
if (selectedCharacter) {
|
||||
if (selectedTemplate) {
|
||||
base.push(
|
||||
`Built-in creative character selected: ${selectedCharacter.name}.`,
|
||||
"Use the built-in images as planned creative direction only; generate an innovative variation, not a duplicate of that character pack.",
|
||||
`Creative subject template selected: ${selectedTemplate.name} (${selectedTemplate.sourceLabel}).`,
|
||||
"Use the template images as planned creative direction only; generate an innovative variation, not a duplicate of that subject pack.",
|
||||
)
|
||||
}
|
||||
if (subjectStyle === "transparent_human") {
|
||||
@@ -523,7 +529,7 @@ function subjectAssetUrl(job: Job, asset: SubjectAsset) {
|
||||
return apiAssetUrl(asset.url) || resolveImageRefUrl(job.id, { kind: "asset", frame_idx: 0, element_id: asset.id })
|
||||
}
|
||||
|
||||
function characterPreviewImage(character?: CharacterLibraryItem | null) {
|
||||
function characterPreviewImage(character?: { primary_image?: string; images?: Array<{ id: string; view?: string; filename: string; label?: string }> } | null) {
|
||||
if (!character?.images?.length) return null
|
||||
return character.images.find((image) => image.id === character.primary_image)
|
||||
?? character.images.find((image) => image.view === "front")
|
||||
@@ -2100,6 +2106,12 @@ function SourceReferenceBuildPanel({
|
||||
const [subjectDirection, setSubjectDirection] = useState("")
|
||||
const [characterLibrary, setCharacterLibrary] = useState<CharacterLibraryItem[]>([])
|
||||
const [selectedCharacterId, setSelectedCharacterId] = useState("")
|
||||
const [subjectTemplateLibrary, setSubjectTemplateLibrary] = useState<SubjectTemplateItem[]>([])
|
||||
const [selectedSubjectTemplateId, setSelectedSubjectTemplateId] = useState("")
|
||||
const [templateLibraryBusy, setTemplateLibraryBusy] = useState(false)
|
||||
const [templateSaveBusy, setTemplateSaveBusy] = useState(false)
|
||||
const [templateDraftName, setTemplateDraftName] = useState("")
|
||||
const [templateDraftNote, setTemplateDraftNote] = 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)),
|
||||
@@ -2117,6 +2129,15 @@ function SourceReferenceBuildPanel({
|
||||
() => characterLibrary.find((character) => character.id === selectedCharacterId) ?? null,
|
||||
[characterLibrary, selectedCharacterId],
|
||||
)
|
||||
const selectedSubjectTemplate = useMemo(
|
||||
() => subjectTemplateLibrary.find((template) => template.id === selectedSubjectTemplateId) ?? null,
|
||||
[subjectTemplateLibrary, selectedSubjectTemplateId],
|
||||
)
|
||||
const selectedTemplatePrompt = selectedSubjectTemplate
|
||||
? { name: selectedSubjectTemplate.name, sourceLabel: "数据库主体模板" }
|
||||
: selectedCharacter
|
||||
? { name: selectedCharacter.name, sourceLabel: "内置策划形象" }
|
||||
: null
|
||||
const visibleActorAssets = useMemo(() => {
|
||||
const latestByView = new Map<string, SubjectAsset>()
|
||||
for (const asset of actorAssets) {
|
||||
@@ -2136,19 +2157,47 @@ function SourceReferenceBuildPanel({
|
||||
: frames.length
|
||||
? `默认使用全部 ${frames.length} 张参考帧`
|
||||
: "待抽帧"
|
||||
const templateSaveHint = visibleActorAssets.length
|
||||
? templateDraftName.trim()
|
||||
? "保存后会进入左侧主体模板库,后续任务可直接复用"
|
||||
: "先给这套主体命名,再保存到主体模板库"
|
||||
: "先生成本次主体视图,再决定是否入库"
|
||||
const templateSourceLabel = selectedSubjectTemplate
|
||||
? `${selectedSubjectTemplate.name} · 数据库模板`
|
||||
: selectedCharacter
|
||||
? `${selectedCharacter.name} · 模板参考`
|
||||
: "源视频关键帧 · 相似创新"
|
||||
|
||||
const loadSubjectTemplateLibrary = async (silent = false) => {
|
||||
setTemplateLibraryBusy(true)
|
||||
try {
|
||||
const items = await listSubjectTemplates()
|
||||
setSubjectTemplateLibrary(items)
|
||||
} catch (e) {
|
||||
if (!silent) toast.error("主体模板库读取失败:" + (e instanceof Error ? e.message : String(e)))
|
||||
} finally {
|
||||
setTemplateLibraryBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
listCharacterLibrary()
|
||||
.then((items) => {
|
||||
if (!cancelled) setCharacterLibrary(items)
|
||||
})
|
||||
.catch((e) => {
|
||||
if (!cancelled) toast.error("内置形象读取失败:" + (e instanceof Error ? e.message : String(e)))
|
||||
Promise.allSettled([listCharacterLibrary(), listSubjectTemplates()])
|
||||
.then(([characters, templates]) => {
|
||||
if (cancelled) return
|
||||
if (characters.status === "fulfilled") setCharacterLibrary(characters.value)
|
||||
else toast.error("内置形象读取失败:" + (characters.reason instanceof Error ? characters.reason.message : String(characters.reason)))
|
||||
if (templates.status === "fulfilled") setSubjectTemplateLibrary(templates.value)
|
||||
else toast.error("主体模板库读取失败:" + (templates.reason instanceof Error ? templates.reason.message : String(templates.reason)))
|
||||
})
|
||||
return () => { cancelled = true }
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setTemplateDraftName("")
|
||||
setTemplateDraftNote("")
|
||||
}, [job.id])
|
||||
|
||||
const generateSimilarActor = async () => {
|
||||
if (!frames.length) {
|
||||
toast.warning("请先自动抽帧 12 张,或在原版视频上手动补帧。")
|
||||
@@ -2163,11 +2212,11 @@ function SourceReferenceBuildPanel({
|
||||
let element = workingFrame.elements?.find(isSimilarActorElement)
|
||||
if (!element) {
|
||||
workingJob = await addElement(job.id, baseFrame.index, {
|
||||
name_zh: selectedCharacter
|
||||
? `相似透明骨架主体 · ${selectedCharacter.name}`
|
||||
name_zh: selectedTemplatePrompt
|
||||
? `相似透明骨架主体 · ${selectedTemplatePrompt.name}`
|
||||
: subjectStyle === "transparent_human" ? "相似透明骨架主体" : "相似广告主角",
|
||||
name_en: selectedCharacter
|
||||
? `similar innovative transparent skeleton humanoid subject based on ${selectedCharacter.name}`
|
||||
name_en: selectedTemplatePrompt
|
||||
? `similar innovative transparent skeleton humanoid subject based on ${selectedTemplatePrompt.name}`
|
||||
: subjectStyle === "transparent_human" ? "similar transparent skeleton humanoid subject" : "similar ad actor",
|
||||
position: "source-video main subject selected from global keyframes",
|
||||
source: "manual",
|
||||
@@ -2188,7 +2237,8 @@ function SourceReferenceBuildPanel({
|
||||
source_frame_indices: subjectReferenceFrames.slice(0, 12).map((frame) => frame.index),
|
||||
views: SUBJECT_ASSET_VIEWS.map((view) => view.value),
|
||||
character_id: selectedCharacterId,
|
||||
prompt: buildSimilarSubjectPrompt(subjectStyle, subjectDirection, selectedCharacter),
|
||||
subject_template_id: selectedSubjectTemplateId,
|
||||
prompt: buildSimilarSubjectPrompt(subjectStyle, subjectDirection, selectedTemplatePrompt),
|
||||
replace_views: true,
|
||||
})
|
||||
onJobUpdate(updated)
|
||||
@@ -2216,7 +2266,8 @@ function SourceReferenceBuildPanel({
|
||||
source_frame_indices: sourceIndices,
|
||||
views: [asset.view],
|
||||
character_id: selectedCharacterId,
|
||||
prompt: buildSimilarSubjectPrompt(subjectStyle, subjectDirection, selectedCharacter),
|
||||
subject_template_id: selectedSubjectTemplateId,
|
||||
prompt: buildSimilarSubjectPrompt(subjectStyle, subjectDirection, selectedTemplatePrompt),
|
||||
replace_views: true,
|
||||
})
|
||||
onJobUpdate(updated)
|
||||
@@ -2242,6 +2293,39 @@ function SourceReferenceBuildPanel({
|
||||
}
|
||||
}
|
||||
|
||||
const saveGeneratedSubjectTemplate = async () => {
|
||||
if (!actorSource || !visibleActorAssets.length) {
|
||||
toast.warning("请先生成相似主体视图。")
|
||||
return
|
||||
}
|
||||
const name = templateDraftName.trim()
|
||||
if (!name) {
|
||||
toast.warning("请先给这套主体模板命名。")
|
||||
return
|
||||
}
|
||||
setTemplateSaveBusy(true)
|
||||
try {
|
||||
const item = await saveSubjectTemplate(job.id, {
|
||||
name,
|
||||
note: templateDraftNote.trim(),
|
||||
frame_idx: actorSource.frame.index,
|
||||
element_id: actorSource.element.id,
|
||||
asset_ids: visibleActorAssets.map((asset) => asset.id),
|
||||
subject_style: subjectStyle,
|
||||
})
|
||||
setSubjectTemplateLibrary((items) => [item, ...items.filter((template) => template.id !== item.id)])
|
||||
setSelectedSubjectTemplateId(item.id)
|
||||
setSelectedCharacterId("")
|
||||
setTemplateDraftName("")
|
||||
setTemplateDraftNote("")
|
||||
toast.success("已保存到主体模板库")
|
||||
} catch (e) {
|
||||
toast.error("保存主体模板失败:" + (e instanceof Error ? e.message : String(e)))
|
||||
} finally {
|
||||
setTemplateSaveBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-w-0">
|
||||
<div className="mb-2 flex items-center justify-between gap-3">
|
||||
@@ -2253,31 +2337,63 @@ function SourceReferenceBuildPanel({
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-md border border-white/10 bg-black/32 p-2">
|
||||
<div>
|
||||
<div className="mb-1.5 flex flex-wrap items-center justify-between gap-2 text-[10px] text-white/36">
|
||||
<div className="flex items-center gap-2">
|
||||
<span>相似主体白底视图</span>
|
||||
<ModelTrace trace={similarSubjectModelTrace(runtimeModels, subjectStyle)} compact />
|
||||
</div>
|
||||
<span className="text-[10px] text-white/32">内置形象只做创意参考,不照抄</span>
|
||||
</div>
|
||||
|
||||
<div className="mb-2">
|
||||
<div className="mb-1 flex items-center justify-between gap-2">
|
||||
<span className="text-[10px] text-white/38">内置形象选择</span>
|
||||
<span className="text-[9px] text-white/28">{selectedCharacter ? `${selectedCharacter.name} · ${selectedCharacter.images.length} 张参考` : "源视频主角相似创新"}</span>
|
||||
<div className="mb-2 grid gap-2 lg:grid-cols-[minmax(360px,1fr)_minmax(300px,0.8fr)]">
|
||||
<div className="rounded-md border border-white/10 bg-black/28 p-2">
|
||||
<div className="mb-1.5 flex items-center justify-between gap-2">
|
||||
<div>
|
||||
<div className="text-[10.5px] font-semibold text-white/70">主体模板库</div>
|
||||
<div className="mt-0.5 text-[9px] text-white/32">数据库模板优先复用;内置形象只作为初始策划模板。</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void loadSubjectTemplateLibrary()}
|
||||
disabled={templateLibraryBusy}
|
||||
className="inline-flex h-6 items-center gap-1 rounded border border-emerald-200/20 bg-emerald-300/10 px-1.5 text-[9px] font-semibold text-emerald-100/80 transition hover:border-emerald-200/40 disabled:cursor-wait disabled:opacity-50"
|
||||
>
|
||||
{templateLibraryBusy ? <Loader2 className="h-3 w-3 animate-spin" /> : <RefreshCw className="h-3 w-3" />}
|
||||
数据库 {subjectTemplateLibrary.length} 套
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-[repeat(auto-fill,minmax(86px,1fr))] gap-1.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSelectedCharacterId("")}
|
||||
onClick={() => {
|
||||
setSelectedCharacterId("")
|
||||
setSelectedSubjectTemplateId("")
|
||||
}}
|
||||
className={`min-h-[58px] rounded-md border px-2 py-1.5 text-left transition ${
|
||||
!selectedCharacterId ? "border-cyan-200/55 bg-cyan-300/12 text-cyan-50" : "border-white/10 bg-black/25 text-white/45 hover:border-white/22 hover:text-white/70"
|
||||
!selectedCharacterId && !selectedSubjectTemplateId ? "border-cyan-200/55 bg-cyan-300/12 text-cyan-50" : "border-white/10 bg-black/25 text-white/45 hover:border-white/22 hover:text-white/70"
|
||||
}`}
|
||||
>
|
||||
<span className="block text-[10.5px] font-semibold">源视频相似</span>
|
||||
<span className="mt-1 block text-[9px] leading-tight opacity-70">根据关键帧创新</span>
|
||||
</button>
|
||||
{subjectTemplateLibrary.map((template) => {
|
||||
const preview = characterPreviewImage(template)
|
||||
const active = selectedSubjectTemplateId === template.id
|
||||
return (
|
||||
<button
|
||||
key={template.id}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setSelectedSubjectTemplateId(template.id)
|
||||
setSelectedCharacterId("")
|
||||
setSubjectStyle(template.subject_style || "transparent_human")
|
||||
}}
|
||||
className={`group flex min-h-[58px] items-center gap-1.5 rounded-md border px-1.5 py-1 text-left transition ${
|
||||
active ? "border-cyan-200/65 bg-cyan-300/12 text-cyan-50" : "border-white/10 bg-black/25 text-white/50 hover:border-cyan-200/35 hover:text-white/80"
|
||||
}`}
|
||||
>
|
||||
<span className="h-12 w-9 shrink-0 overflow-hidden rounded border border-white/10 bg-white">
|
||||
{preview ? <img src={subjectTemplateImageUrl(preview.filename)} alt={template.name} className="h-full w-full object-cover" /> : null}
|
||||
</span>
|
||||
<span className="min-w-0">
|
||||
<span className="block truncate text-[10px] font-semibold">{template.name}</span>
|
||||
<span className="mt-0.5 block text-[8.5px] opacity-58">数据库 · {template.images.length} 图</span>
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
{characterLibrary.map((character) => {
|
||||
const preview = characterPreviewImage(character)
|
||||
const active = selectedCharacterId === character.id
|
||||
@@ -2287,6 +2403,7 @@ function SourceReferenceBuildPanel({
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setSelectedCharacterId(character.id)
|
||||
setSelectedSubjectTemplateId("")
|
||||
setSubjectStyle("transparent_human")
|
||||
}}
|
||||
className={`group flex min-h-[58px] items-center gap-1.5 rounded-md border px-1.5 py-1 text-left transition ${
|
||||
@@ -2298,13 +2415,26 @@ function SourceReferenceBuildPanel({
|
||||
</span>
|
||||
<span className="min-w-0">
|
||||
<span className="block truncate text-[10px] font-semibold">{character.name}</span>
|
||||
<span className="mt-0.5 block text-[8.5px] opacity-58">7 图参考</span>
|
||||
<span className="mt-0.5 block text-[8.5px] opacity-58">内置 · 7 图</span>
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{selectedCharacter?.images?.length ? (
|
||||
{!subjectTemplateLibrary.length ? (
|
||||
<div className="mt-1.5 rounded border border-dashed border-white/10 px-2 py-1.5 text-[9px] leading-snug text-white/28">
|
||||
数据库暂未保存主体。生成满意的相似主体后,在右侧命名并保存,后续会出现在这里。
|
||||
</div>
|
||||
) : null}
|
||||
{selectedSubjectTemplate?.images?.length ? (
|
||||
<div className="mt-1.5 flex gap-1 overflow-x-auto pb-0.5">
|
||||
{selectedSubjectTemplate.images.slice(0, 10).map((image) => (
|
||||
<div key={image.id} className="h-12 w-9 shrink-0 overflow-hidden rounded border border-white/10 bg-white" title={image.label}>
|
||||
<img src={subjectTemplateImageUrl(image.filename)} alt={image.label} className="h-full w-full object-cover" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : selectedCharacter?.images?.length ? (
|
||||
<div className="mt-1.5 flex gap-1 overflow-x-auto pb-0.5">
|
||||
{selectedCharacter.images.slice(0, 7).map((image) => (
|
||||
<div key={image.id} className="h-12 w-9 shrink-0 overflow-hidden rounded border border-white/10 bg-white" title={image.label}>
|
||||
@@ -2315,6 +2445,60 @@ function SourceReferenceBuildPanel({
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border border-white/10 bg-black/28 p-2">
|
||||
<div className="mb-1.5 flex flex-wrap items-start justify-between gap-2">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 text-[10.5px] font-semibold text-white/70">
|
||||
<span>本次生成 / 入库草稿</span>
|
||||
<ModelTrace trace={similarSubjectModelTrace(runtimeModels, subjectStyle)} compact />
|
||||
</div>
|
||||
<div className="mt-0.5 text-[9px] text-white/32">{templateSourceLabel} · {visibleActorAssets.length}/{SUBJECT_ASSET_VIEWS.length} 张</div>
|
||||
</div>
|
||||
<span className={`rounded border px-1.5 py-0.5 text-[9px] font-semibold ${
|
||||
visibleActorAssets.length ? "border-emerald-200/25 bg-emerald-300/10 text-emerald-100/80" : "border-white/10 bg-white/5 text-white/36"
|
||||
}`}>
|
||||
{visibleActorAssets.length ? "可命名待入库" : "未生成"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid gap-1.5">
|
||||
<input
|
||||
value={templateDraftName}
|
||||
onChange={(event) => setTemplateDraftName(event.target.value)}
|
||||
placeholder="模板命名:如透明骨架女性 01"
|
||||
className="h-7 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"
|
||||
/>
|
||||
<textarea
|
||||
value={templateDraftNote}
|
||||
onChange={(event) => setTemplateDraftNote(event.target.value)}
|
||||
placeholder="备注:适合什么广告、人物年龄/性别/材质、禁用点"
|
||||
className="min-h-[46px] resize-none rounded-md border border-white/10 bg-black/35 px-2 py-1.5 text-[10.5px] leading-snug text-white outline-none placeholder:text-white/28 focus:border-cyan-300/50"
|
||||
/>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="min-w-0 text-[9px] leading-snug text-white/32">{templateSaveHint}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void saveGeneratedSubjectTemplate()}
|
||||
disabled={!visibleActorAssets.length || !templateDraftName.trim() || templateSaveBusy}
|
||||
title={!visibleActorAssets.length ? "先生成主体视图" : !templateDraftName.trim() ? "先填写模板名称" : "保存到主体模板库"}
|
||||
className="inline-flex h-7 shrink-0 items-center justify-center gap-1 rounded-md border border-emerald-200/25 bg-emerald-300/12 px-2 text-[10px] font-semibold text-emerald-50 transition hover:border-emerald-200/45 hover:bg-emerald-300/18 disabled:cursor-not-allowed disabled:border-white/10 disabled:bg-white/6 disabled:text-white/32"
|
||||
>
|
||||
{templateSaveBusy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Plus className="h-3.5 w-3.5" />}
|
||||
保存到主体库
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="mb-1.5 flex flex-wrap items-center justify-between gap-2 text-[10px] text-white/36">
|
||||
<div className="flex items-center gap-2">
|
||||
<span>主体生成设置</span>
|
||||
<span className="text-white/28">{referenceCountLabel}</span>
|
||||
</div>
|
||||
<span className="text-[10px] text-white/32">模板只做创意参考;生成后人工确认,再决定是否入库复用。</span>
|
||||
</div>
|
||||
|
||||
<div className="mb-1.5 flex flex-wrap items-center justify-end gap-2 text-[10px] text-white/36">
|
||||
<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">
|
||||
|
||||
@@ -359,6 +359,49 @@ export function characterLibraryImageUrl(filename: string): string {
|
||||
return `${API_BASE}/character-library/skg/images/${filename}`
|
||||
}
|
||||
|
||||
export async function listSubjectTemplates(): Promise<SubjectTemplateItem[]> {
|
||||
const res = await fetch(`${API_BASE}/subject-templates`)
|
||||
if (!res.ok) {
|
||||
const txt = await res.text().catch(() => "")
|
||||
throw new Error(`listSubjectTemplates ${res.status} ${txt.slice(0, 300)}`)
|
||||
}
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export function subjectTemplateImageUrl(filename: string): string {
|
||||
return `${API_BASE}/subject-templates/images/${filename}`
|
||||
}
|
||||
|
||||
export async function saveSubjectTemplate(
|
||||
jobId: string,
|
||||
body: {
|
||||
name: string
|
||||
note?: string
|
||||
frame_idx: number
|
||||
element_id: string
|
||||
asset_ids: string[]
|
||||
subject_style?: "transparent_human" | "source_actor"
|
||||
},
|
||||
): Promise<SubjectTemplateItem> {
|
||||
const res = await fetch(`${API_BASE}/jobs/${jobId}/subject-templates`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
name: body.name,
|
||||
note: body.note ?? "",
|
||||
frame_idx: body.frame_idx,
|
||||
element_id: body.element_id,
|
||||
asset_ids: body.asset_ids,
|
||||
subject_style: body.subject_style ?? "transparent_human",
|
||||
}),
|
||||
})
|
||||
if (!res.ok) {
|
||||
const txt = await res.text().catch(() => "")
|
||||
throw new Error(`saveSubjectTemplate ${res.status} ${txt.slice(0, 300)}`)
|
||||
}
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export async function copyCharacterLibraryAssets(jobId: string, characterId: string): Promise<{ character_id: string; character_name: string; images: ImageRef[] }> {
|
||||
const res = await fetch(`${API_BASE}/jobs/${jobId}/assets/character-library`, {
|
||||
method: "POST",
|
||||
@@ -520,6 +563,38 @@ export interface CharacterLibraryItem {
|
||||
images: CharacterLibraryImage[]
|
||||
}
|
||||
|
||||
export interface SubjectTemplateImage {
|
||||
id: string
|
||||
view: string
|
||||
label: string
|
||||
filename: string
|
||||
url: string
|
||||
width: number
|
||||
height: number
|
||||
background: "white" | "black"
|
||||
quality: "hd"
|
||||
size: "source" | "1024" | "1536" | "2048"
|
||||
source_asset_id: string
|
||||
source_frame_indices: number[]
|
||||
created_at: number
|
||||
}
|
||||
|
||||
export interface SubjectTemplateItem {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
note: string
|
||||
source: "database"
|
||||
source_job_id: string
|
||||
source_frame_idx: number
|
||||
source_element_id: string
|
||||
subject_style: "transparent_human" | "source_actor"
|
||||
primary_image: string
|
||||
images: SubjectTemplateImage[]
|
||||
created_at: number
|
||||
updated_at: number
|
||||
}
|
||||
|
||||
export interface TranscriptSegment {
|
||||
index: number
|
||||
start: number
|
||||
@@ -1087,6 +1162,7 @@ export async function generateSubjectAssets(
|
||||
source_frame_indices?: number[]
|
||||
views?: string[]
|
||||
character_id?: string
|
||||
subject_template_id?: string
|
||||
subject_style?: "transparent_human" | "source_actor"
|
||||
reconstruction_mode?: "same" | "similar"
|
||||
prompt?: string
|
||||
@@ -1104,6 +1180,7 @@ export async function generateSubjectAssets(
|
||||
source_frame_indices: body.source_frame_indices ?? null,
|
||||
views: body.views ?? null,
|
||||
character_id: body.character_id ?? "",
|
||||
subject_template_id: body.subject_template_id ?? "",
|
||||
subject_style: body.subject_style ?? "transparent_human",
|
||||
reconstruction_mode: body.reconstruction_mode ?? "same",
|
||||
prompt: body.prompt ?? "",
|
||||
|
||||
Reference in New Issue
Block a user