feat: 同步角色选角与首帧身份约束
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { AppDialog } from '../../../components/ui'
|
||||
import { validImageSize } from '../../subject-images/model'
|
||||
import type { GenerateCastingCandidateInput } from '../types'
|
||||
|
||||
/** Character 选角候选表单固定使用 primary,且不会自动选择演员。 */
|
||||
const props = defineProps<{ subjectId: string; subjectName: string; disabled: boolean }>()
|
||||
const open = defineModel<boolean>('open', { default: false })
|
||||
const emit = defineEmits<{ generate: [input: GenerateCastingCandidateInput] }>()
|
||||
const prompt = ref('')
|
||||
const width = ref<number | ''>('')
|
||||
const height = ref<number | ''>('')
|
||||
const acknowledged = ref(false)
|
||||
const valid = computed(() => validImageSize(width.value, height.value))
|
||||
|
||||
/** 每次切换角色或重新打开都清空自定义覆盖与费用确认。 */
|
||||
function reset() {
|
||||
prompt.value = ''
|
||||
width.value = ''
|
||||
height.value = ''
|
||||
acknowledged.value = false
|
||||
}
|
||||
watch([() => props.subjectId, open], reset)
|
||||
|
||||
/** 只发送选角候选接口接受的字段,不携带普通身份图视角参数。 */
|
||||
function submit() {
|
||||
if (props.disabled || !valid.value || !acknowledged.value) return
|
||||
emit('generate', {
|
||||
provider: 'seedream',
|
||||
...(prompt.value.trim() ? { prompt: prompt.value.trim() } : {}),
|
||||
...(width.value !== '' && height.value !== '' ? { width: width.value, height: height.value } : {})
|
||||
})
|
||||
open.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppDialog v-model:open="open" title="生成角色选角候选" :description="`${subjectName} · 候选不会自动成为身份母版`">
|
||||
<form class="mt-5 space-y-4" @submit.prevent="submit">
|
||||
<p class="alert text-xs">
|
||||
后端会依据角色 Identity、项目视觉风格和默认选角约束生成一张 primary
|
||||
候选。生成完成后仍需人工点击“确认选角”。
|
||||
</p>
|
||||
<label class="block">
|
||||
<span class="field-label">覆盖本次完整提示词(可选)</span>
|
||||
<textarea
|
||||
id="casting-candidate-prompt"
|
||||
v-model="prompt"
|
||||
class="input min-h-24"
|
||||
placeholder="建议留空,使用后端编译的稳定身份和选角约束"
|
||||
/>
|
||||
</label>
|
||||
<p v-if="prompt.trim()" class="text-xs text-danger">
|
||||
自定义文本会替换后端完整提示词,请自行包含人物身份、基础服装和中性参考图要求。
|
||||
</p>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<label
|
||||
><span class="field-label">宽度(px)</span
|
||||
><input v-model.number="width" class="input" type="number" min="1" placeholder="后端默认"
|
||||
/></label>
|
||||
<label
|
||||
><span class="field-label">高度(px)</span
|
||||
><input v-model.number="height" class="input" type="number" min="1" placeholder="后端默认"
|
||||
/></label>
|
||||
</div>
|
||||
<p v-if="!valid" class="text-xs text-danger">宽高需要同时留空,或同时填写正整数。</p>
|
||||
<label class="flex items-start gap-2 text-xs leading-6">
|
||||
<input
|
||||
id="casting-candidate-cost"
|
||||
v-model="acknowledged"
|
||||
type="checkbox"
|
||||
class="mt-1.5 accent-accent"
|
||||
/>我已确认调用 Seedream 可能产生费用,且当前没有同项目生成任务。
|
||||
</label>
|
||||
<div class="dialog-footer">
|
||||
<button type="button" class="button button-secondary" @click="open = false">取消</button>
|
||||
<button type="submit" class="button button-primary" :disabled="disabled || !valid || !acknowledged">
|
||||
确认生成候选
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</AppDialog>
|
||||
</template>
|
||||
@@ -61,12 +61,11 @@ function save() {
|
||||
这里保存身份事实;背景、视角和参考图约束由后端另行编译,不是最终整段生图 Prompt。
|
||||
</p>
|
||||
<label class="flex items-start gap-2 text-xs leading-6"
|
||||
><input
|
||||
id="identity-lock"
|
||||
v-model="form.isLocked"
|
||||
type="checkbox"
|
||||
class="mt-1.5 accent-accent"
|
||||
/>确认并锁定身份文本。锁定后仍可人工保存、生成图片及切换母版。</label
|
||||
><input id="identity-lock" v-model="form.isLocked" type="checkbox" class="mt-1.5 accent-accent" />{{
|
||||
module === 'character'
|
||||
? '确认并锁定演员身份。Character 必须同时具有身份母版并锁定 Identity,首帧才能进入就绪状态。'
|
||||
: '确认并锁定身份文本。锁定后仍可人工保存、生成图片及切换母版。'
|
||||
}}</label
|
||||
>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<span class="text-xs text-muted">{{
|
||||
|
||||
@@ -7,7 +7,13 @@ import { canBeAnchor, currentAnchor, identityViewLabels, readImageProvenance } f
|
||||
import type { IdentityImage } from '../types'
|
||||
|
||||
/** 身份图库区分权威母版、primary 候选和辅助视图,保留后端原始状态。 */
|
||||
const props = defineProps<{ images: IdentityImage[]; subjectName: string; disabled: boolean }>()
|
||||
const props = defineProps<{
|
||||
images: IdentityImage[]
|
||||
subjectName: string
|
||||
module: string
|
||||
identityLocked: boolean
|
||||
disabled: boolean
|
||||
}>()
|
||||
const emit = defineEmits<{ anchor: [imageId: string] }>()
|
||||
const selectedId = ref('')
|
||||
const confirming = ref(false)
|
||||
@@ -15,8 +21,13 @@ const anchor = computed(() => currentAnchor(props.images))
|
||||
const selected = computed(
|
||||
() => props.images.find(image => image.id === selectedId.value) ?? anchor.value ?? props.images[0]
|
||||
)
|
||||
const character = computed(() => props.module === 'character')
|
||||
const canSelect = computed(
|
||||
() => !props.disabled && !!selected.value && canBeAnchor(selected.value) && !selected.value.isAnchor
|
||||
() =>
|
||||
!props.disabled &&
|
||||
!!selected.value &&
|
||||
canBeAnchor(selected.value) &&
|
||||
(!selected.value.isAnchor || (character.value && !props.identityLocked))
|
||||
)
|
||||
const imageUrl = computed(() => referenceImageUrl(selected.value?.imageUrl ?? ''))
|
||||
watch(
|
||||
@@ -76,12 +87,12 @@ function chooseAnchor() {
|
||||
<p v-if="selected.error" class="alert alert-error mt-3" role="alert">{{ selected.error }}</p>
|
||||
<div class="mt-3 flex flex-wrap items-center gap-3">
|
||||
<button
|
||||
v-if="!selected.isAnchor"
|
||||
v-if="!selected.isAnchor || (character && !identityLocked)"
|
||||
class="button button-secondary"
|
||||
:disabled="!canSelect"
|
||||
@click="confirming = true"
|
||||
>
|
||||
设为身份母版</button
|
||||
{{ character ? '确认选角并锁定' : '设为身份母版' }}</button
|
||||
><span class="text-[11px] text-muted"
|
||||
>{{ selected.provider }} · {{ selected.model }} · {{ formatDate(selected.createdAt) }}</span
|
||||
>
|
||||
@@ -91,12 +102,15 @@ function chooseAnchor() {
|
||||
</p>
|
||||
<div v-if="confirming" class="alert mt-3">
|
||||
<p class="text-xs">
|
||||
确认切换身份母版?旧 primary
|
||||
母版将停用并保留。后续人物/场景形态生图会引用新母版,道具暂不自动引用。此操作不调用模型。
|
||||
{{
|
||||
character
|
||||
? '确认选择这张图片作为正式演员?后端会在同一事务中切换身份母版并锁定 Identity,完成后首帧才具备稳定人物身份。'
|
||||
: '确认切换身份母版?旧 primary 母版将停用并保留。后续场景形态生图会引用新母版。此操作不调用模型。'
|
||||
}}
|
||||
</p>
|
||||
<div class="mt-3 flex gap-3">
|
||||
<button class="button button-primary" :disabled="!canSelect" @click="chooseAnchor">
|
||||
确认切换身份母版</button
|
||||
{{ character ? '确认演员选择' : '确认切换身份母版' }}</button
|
||||
><button class="text-button" @click="confirming = false">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,13 @@ import { currentAnchor, identityViewLabels } from '../model'
|
||||
import type { GenerateIdentityImageInput, IdentityImage, IdentityViewType } from '../types'
|
||||
|
||||
/** 身份图生成配置;默认自动引用母版,不提供后端不存在的“忽略母版”开关。 */
|
||||
const props = defineProps<{ subjectId: string; subjectName: string; images: IdentityImage[]; disabled: boolean }>()
|
||||
const props = defineProps<{
|
||||
subjectId: string
|
||||
subjectName: string
|
||||
module?: string
|
||||
images: IdentityImage[]
|
||||
disabled: boolean
|
||||
}>()
|
||||
const open = defineModel<boolean>('open', { default: false })
|
||||
const emit = defineEmits<{ generate: [input: GenerateIdentityImageInput] }>()
|
||||
const viewType = ref<IdentityViewType>('primary')
|
||||
@@ -16,6 +22,9 @@ const width = ref<number | ''>('')
|
||||
const height = ref<number | ''>('')
|
||||
const acknowledged = ref(false)
|
||||
const anchor = computed(() => currentAnchor(props.images))
|
||||
const allowedViews = computed(() =>
|
||||
Object.entries(identityViewLabels).filter(([value]) => props.module !== 'character' || value !== 'primary')
|
||||
)
|
||||
const references = computed(() => props.images.filter(image => image.status === 'completed' && image.imageUrl))
|
||||
const valid = computed(
|
||||
() =>
|
||||
@@ -25,13 +34,14 @@ const valid = computed(
|
||||
watch(
|
||||
() => [open.value, props.subjectId],
|
||||
() => {
|
||||
viewType.value = 'primary'
|
||||
viewType.value = props.module === 'character' ? 'front' : 'primary'
|
||||
referenceImageId.value = ''
|
||||
prompt.value = ''
|
||||
width.value = ''
|
||||
height.value = ''
|
||||
acknowledged.value = false
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
/** 提交显式的 Seedream 参数;省略空参考 ID 以保留后端自动选择逻辑。 */
|
||||
@@ -58,7 +68,7 @@ function submit() {
|
||||
<label class="block"
|
||||
><span class="field-label">参考视角</span
|
||||
><select id="identity-view" v-model="viewType" class="input">
|
||||
<option v-for="(label, value) in identityViewLabels" :key="value" :value="value">
|
||||
<option v-for="[value, label] in allowedViews" :key="value" :value="value">
|
||||
{{ label }}({{ value }})
|
||||
</option>
|
||||
</select></label
|
||||
|
||||
Reference in New Issue
Block a user