feat: 同步最新生图与身份能力
This commit is contained in:
@@ -6,6 +6,7 @@ import type {
|
||||
GenerateQualityVideosInput,
|
||||
KeyframeBatchResult,
|
||||
KeyframeReadiness,
|
||||
KeyframeGenerationPreview,
|
||||
KeyframeSpec,
|
||||
ProjectVideoStatus,
|
||||
PromptReadiness,
|
||||
@@ -106,6 +107,9 @@ export const productionApi = {
|
||||
request<ShotKeyframe[]>(`${shotPath(shotId)}/keyframes`, { signal }),
|
||||
keyframeSpec: (shotId: string, signal?: AbortSignal) =>
|
||||
request<KeyframeSpec>(`${shotPath(shotId)}/keyframe-spec`, { signal }),
|
||||
/** 预览最终参考图规划与 Provider Prompt,不创建首帧任务。 */
|
||||
previewKeyframe: (shotId: string) =>
|
||||
request<KeyframeGenerationPreview>(`${shotPath(shotId)}/keyframe/preview`, { method: 'POST', body: {} }),
|
||||
generateKeyframe: (shotId: string, input: GenerateKeyframeInput) =>
|
||||
request<ShotKeyframe | null>(`${shotPath(shotId)}/keyframe`, {
|
||||
method: 'POST',
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
import AppForm from '../../../components/ui/AppForm.vue'
|
||||
import { NFormItem } from 'naive-ui'
|
||||
import { sizeRules } from '../../../lib/form-rules'
|
||||
import { NAlert, NButton, NCheckbox, NInputNumber } from 'naive-ui'
|
||||
import { NAlert, NButton, NCheckbox, NCollapse, NCollapseItem, NInputNumber, NTag } from 'naive-ui'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { AppDialog } from '../../../components/ui'
|
||||
import { errorMessage } from '../../../lib/http'
|
||||
import { productionApi } from '../api'
|
||||
import { validOptionalSize } from '../model'
|
||||
import type { GenerateKeyframeInput, ShotKeyframe } from '../types'
|
||||
import type { GenerateKeyframeInput, KeyframeGenerationPreview, ShotKeyframe } from '../types'
|
||||
|
||||
/** 单镜头首帧表单;普通候选仍由用户明确选择,过期主首帧默认替换为新图。 */
|
||||
const props = defineProps<{
|
||||
@@ -22,6 +24,9 @@ const width = ref<number | ''>('')
|
||||
const height = ref<number | ''>('')
|
||||
const setPrimary = ref(false)
|
||||
const confirmed = ref(false)
|
||||
const preview = ref<KeyframeGenerationPreview | null>(null)
|
||||
const previewError = ref('')
|
||||
const previewPending = ref(false)
|
||||
const hasPrimary = computed(() => props.keyframes.some(item => item.isPrimary && item.status === 'completed'))
|
||||
const sizeValid = computed(() => validOptionalSize(width.value, height.value))
|
||||
const canSubmit = computed(() => !props.disabled && sizeValid.value && confirmed.value)
|
||||
@@ -32,6 +37,9 @@ function reset() {
|
||||
height.value = ''
|
||||
setPrimary.value = !!props.replacePrimary || !hasPrimary.value
|
||||
confirmed.value = false
|
||||
preview.value = null
|
||||
previewError.value = ''
|
||||
previewPending.value = false
|
||||
}
|
||||
watch([() => props.shotId, open], reset, { immediate: true })
|
||||
|
||||
@@ -47,6 +55,23 @@ function submit() {
|
||||
}
|
||||
const formModel = computed(() => ({ width: width.value, height: height.value }))
|
||||
const rules = sizeRules(() => formModel.value)
|
||||
|
||||
/** 读取后端最终参考图选择与 Prompt,不创建首帧候选。 */
|
||||
async function loadPreview() {
|
||||
if (!props.shotId || previewPending.value) return
|
||||
previewPending.value = true
|
||||
previewError.value = ''
|
||||
try {
|
||||
const result = await productionApi.previewKeyframe(props.shotId)
|
||||
if (!result || result.shotId !== props.shotId) throw new Error('预览结果与当前镜头不匹配。')
|
||||
preview.value = result
|
||||
} catch (cause) {
|
||||
preview.value = null
|
||||
previewError.value = errorMessage(cause)
|
||||
} finally {
|
||||
previewPending.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -66,6 +91,49 @@ const rules = sizeRules(() => formModel.value)
|
||||
<NAlert v-if="replacePrimary" type="info" :show-icon="false" class="mt-5 text-xs">
|
||||
当前主首帧使用的主体参考资产已经变化。新图成功后应设为主首帧,旧图会继续保留为历史候选。
|
||||
</NAlert>
|
||||
<div class="mt-5 flex flex-wrap items-center gap-3">
|
||||
<NButton :loading="previewPending" @click="loadPreview">预览实际生成输入</NButton>
|
||||
<span class="text-xs text-muted">只读取参考图规划与最终 Prompt,不调用图片模型。</span>
|
||||
</div>
|
||||
<NAlert v-if="previewError" type="error" :show-icon="false" class="mt-3">{{ previewError }}</NAlert>
|
||||
<div v-if="preview" class="panel mt-3 p-4 text-xs leading-6">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<strong>{{ preview.provider }} / {{ preview.model }}</strong>
|
||||
<NTag size="small" :bordered="false">
|
||||
已选 {{ preview.referencePlan.selected.length }} /
|
||||
{{ preview.referencePlan.totalCandidates }} 张
|
||||
</NTag>
|
||||
<NTag v-if="preview.referencePlan.omitted.length" size="small" type="warning" :bordered="false">
|
||||
省略 {{ preview.referencePlan.omitted.length }} 张
|
||||
</NTag>
|
||||
</div>
|
||||
<ul v-if="preview.referencePlan.selected.length" class="mt-3 space-y-1 text-muted">
|
||||
<li v-for="item in preview.referencePlan.selected" :key="`${item.role}:${item.imageId}`">
|
||||
{{ item.subjectRef }} · {{ item.role }} · <code>{{ item.imageId }}</code>
|
||||
</li>
|
||||
</ul>
|
||||
<NCollapse class="mt-3" :default-expanded-names="['prompt']">
|
||||
<NCollapseItem name="prompt" title="最终 Provider Prompt">
|
||||
<p class="mt-3 whitespace-pre-wrap">{{ preview.providerPrompt }}</p>
|
||||
</NCollapseItem>
|
||||
<NCollapseItem
|
||||
v-if="preview.referencePlan.omitted.length"
|
||||
name="omitted"
|
||||
title="未进入模型的参考图"
|
||||
>
|
||||
<ul class="mt-3 space-y-1 text-muted">
|
||||
<li v-for="item in preview.referencePlan.omitted" :key="`${item.role}:${item.imageId}`">
|
||||
{{ item.subjectRef }} · {{ item.role }} · <code>{{ item.imageId }}</code>
|
||||
</li>
|
||||
</ul>
|
||||
</NCollapseItem>
|
||||
<NCollapseItem name="config" title="生成配置">
|
||||
<pre class="mt-3 whitespace-pre-wrap">{{
|
||||
JSON.stringify(preview.generationConfig, null, 2)
|
||||
}}</pre>
|
||||
</NCollapseItem>
|
||||
</NCollapse>
|
||||
</div>
|
||||
<div class="mt-5 grid grid-cols-2 gap-4">
|
||||
<NFormItem path="width" label="宽度(可选)"
|
||||
><NInputNumber
|
||||
|
||||
@@ -146,6 +146,53 @@ export interface KeyframeSpec {
|
||||
references: KeyframeReference[]
|
||||
}
|
||||
|
||||
export type ImageReferenceRole =
|
||||
| 'style-overall'
|
||||
| 'style-character'
|
||||
| 'style-scene'
|
||||
| 'style-prop'
|
||||
| 'identity-anchor'
|
||||
| 'form-primary'
|
||||
|
||||
export interface KeyframePreviewReference {
|
||||
originalImageNumber: number
|
||||
subjectRef: string
|
||||
module: string
|
||||
role: ImageReferenceRole
|
||||
imageId: string
|
||||
imageUrl: string
|
||||
}
|
||||
|
||||
export interface KeyframeReferencePlanItem {
|
||||
subjectId?: string
|
||||
subjectRef: string
|
||||
module: string
|
||||
role: ImageReferenceRole
|
||||
imageId: string
|
||||
subjectFormId?: string
|
||||
}
|
||||
|
||||
/** 首帧生成前的无费用参考图规划与 Provider Prompt 预览。 */
|
||||
export interface KeyframeGenerationPreview {
|
||||
shotId: string
|
||||
episodeNo: number
|
||||
beatNo: number
|
||||
shotNo: number
|
||||
provider: string
|
||||
model: string
|
||||
generationConfig: Record<string, unknown>
|
||||
references: KeyframePreviewReference[]
|
||||
referencePlan: {
|
||||
provider: string
|
||||
maxReferenceImages: number | null
|
||||
totalCandidates: number
|
||||
strategy: 'all' | 'subject-coverage' | 'repair-aware'
|
||||
selected: KeyframeReferencePlanItem[]
|
||||
omitted: KeyframeReferencePlanItem[]
|
||||
}
|
||||
providerPrompt: string
|
||||
}
|
||||
|
||||
/** 视频生成规格中的主体参考图。 */
|
||||
export interface VideoReference {
|
||||
/** 当前目标视频模型的授权素材,不拼接模型私有地址。 */
|
||||
|
||||
Reference in New Issue
Block a user