feat: 同步最新生图与身份能力

This commit is contained in:
GouJ
2026-09-22 19:56:12 +08:00
parent 65e8f9c3ba
commit be06caaa9f
18 changed files with 583 additions and 69 deletions
+4
View File
@@ -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
+47
View File
@@ -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 {
/** 当前目标视频模型的授权素材,不拼接模型私有地址。 */