From be06caaa9fd6cc716b65ffb6495209a86ea93ea8 Mon Sep 17 00:00:00 2001 From: GouJ Date: Tue, 22 Sep 2026 19:56:12 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=90=8C=E6=AD=A5=E6=9C=80=E6=96=B0?= =?UTF-8?q?=E7=94=9F=E5=9B=BE=E4=B8=8E=E8=BA=AB=E4=BB=BD=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/api-contract-sync.md | 7 ++ docs/backend-coverage.md | 12 ++ src/features/production/api.ts | 4 + .../production/components/KeyframeDialog.vue | 72 +++++++++++- src/features/production/types.ts | 47 ++++++++ .../subject-identity/SubjectIdentityPage.vue | 79 ++++++++++++- src/features/subject-identity/api.ts | 13 ++- src/features/subject-identity/types.ts | 23 ++++ .../subject-identity/useSubjectIdentity.ts | 110 ++++++++++++------ .../subject-images/SubjectImagesPage.vue | 16 +++ src/features/subject-images/api.ts | 6 +- .../components/GenerateImageDialog.vue | 56 ++++++++- src/features/subject-images/types.ts | 20 ++++ .../subject-images/useSubjectImages.ts | 61 ++++++---- tests/features/production/production.test.ts | 55 +++++++++ tests/features/subject-images/images.test.ts | 35 ++++++ tests/features/subject-images/prompts.test.ts | 8 ++ tests/lib/http.test.ts | 28 ++++- 18 files changed, 583 insertions(+), 69 deletions(-) diff --git a/docs/api-contract-sync.md b/docs/api-contract-sync.md index de4a0d4..5406faf 100644 --- a/docs/api-contract-sync.md +++ b/docs/api-contract-sync.md @@ -52,3 +52,10 @@ - 素材公开字段包含 `publicUrl`、类型、MIME、扩展名、大小、分类、元数据和时间;页面只读取 `metadata.originalName` 作为辅助搜索,不依赖内部存储路径。 - 视觉风格新增参考图可发送 `projectAssetId`,后端从所属项目素材解析 `imageUrl`;外部 `imageUrl` 入口继续兼容。资产关联关系不在公开风格图 DTO 中推断。 - 删除仍由后端执行引用保护;前端不先删风格记录,也不将失败响应显示为已删除。 + +## 2026-09-22 增量同步 + +- 新增形态图和镜头首帧的 POST 预览接口;请求体默认 `{}`,只读取后端当前生成配置与参考图规划,不产生模型任务。 +- 首帧预览按 `referencePlan.selected`/`omitted` 展示 Provider 上限后的真实输入;不再从通用 `keyframe-spec` 推断最终选图。 +- 形态提示词、形态图片及主体身份文本批处理均可发送可选 `module`;省略时保持全项目行为。 +- 场景/道具身份母版使用 `/projects/:projectId/subject-identities/anchors/generate`,请求包含 `module`、`limit`、`concurrency`;成功项已由后端设为 Anchor 并锁定,不再由前端逐项补 PUT。 diff --git a/docs/backend-coverage.md b/docs/backend-coverage.md index 52aebbc..72b57e8 100644 --- a/docs/backend-coverage.md +++ b/docs/backend-coverage.md @@ -83,3 +83,15 @@ 核对后端 dev `d9ca93ef54a4c5fbe3917c504531a45f31cb9966`。新增独立项目资产库页面,接入图片上传、列表筛选、详情预览、名称/分类编辑和删除;上传使用 multipart,前端与后端同时限制 JPEG、PNG、WebP 及单张 20MB。视觉风格页可按需读取资产库并以正式 `projectAssetId` 登记参考图,仍保留外部 URL 兼容入口。后端负责校验资产归属、图片类型和删除引用保护。 启用的视觉风格参考图会由后端编译为形态生图的分类参考职责,身份母版继续承担主体一致性。前端不拼接 Provider Prompt,也不把资产库图片自动设为形态主图;已有形态图不会因资产或风格参考变化被客户端静默替换。 + +## 2026-09-22 生图预览与主体模块批处理 + +核对后端 dev `9d927cd3e9d7e01415ad953bf3a0957bc3825050`,补齐以下正式能力: + +- 形态生图弹窗接入 POST `/subject-forms/:subjectFormId/images/preview`,展示后端实际 Provider、模型、参考图顺序、生成配置与最终 Prompt;预览不创建生图任务。 +- 首帧弹窗接入 POST `/storyboard-shots/:shotId/keyframe/preview`,展示视觉风格、身份母版和形态主图经过去重与 Provider 上限规划后的实际选中/省略结果。 +- 形态提示词与形态图片批处理支持 `module=character|scene|prop`,前端提供统一批处理范围选择,不再只能执行全项目全部类型。 +- 身份文本批处理支持模块筛选;场景/道具新增稳定母版小批量入口,成功后由后端原子设为 Anchor 并锁定 Identity,前端保留部分失败回执。 +- 全局 GET `/assets` 已由现有资产库页面使用,无需新增重复页面。 + +预览结果只用于提交前核对,不作为持久化任务或质量通过证明;真正生成仍需费用确认,并以刷新后的正式资产记录为准。 diff --git a/src/features/production/api.ts b/src/features/production/api.ts index 408b6ee..045869f 100644 --- a/src/features/production/api.ts +++ b/src/features/production/api.ts @@ -6,6 +6,7 @@ import type { GenerateQualityVideosInput, KeyframeBatchResult, KeyframeReadiness, + KeyframeGenerationPreview, KeyframeSpec, ProjectVideoStatus, PromptReadiness, @@ -106,6 +107,9 @@ export const productionApi = { request(`${shotPath(shotId)}/keyframes`, { signal }), keyframeSpec: (shotId: string, signal?: AbortSignal) => request(`${shotPath(shotId)}/keyframe-spec`, { signal }), + /** 预览最终参考图规划与 Provider Prompt,不创建首帧任务。 */ + previewKeyframe: (shotId: string) => + request(`${shotPath(shotId)}/keyframe/preview`, { method: 'POST', body: {} }), generateKeyframe: (shotId: string, input: GenerateKeyframeInput) => request(`${shotPath(shotId)}/keyframe`, { method: 'POST', diff --git a/src/features/production/components/KeyframeDialog.vue b/src/features/production/components/KeyframeDialog.vue index c5915c8..9047663 100644 --- a/src/features/production/components/KeyframeDialog.vue +++ b/src/features/production/components/KeyframeDialog.vue @@ -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('') const height = ref('') const setPrimary = ref(false) const confirmed = ref(false) +const preview = ref(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 + } +}