refactor: 同步独立选角与默认人物基线

This commit is contained in:
GouJ
2026-09-01 10:11:15 +08:00
parent 53917268d3
commit 95bd12cfcd
9 changed files with 25 additions and 14 deletions
+3 -2
View File
@@ -86,7 +86,7 @@ Oxfmt 不负责代码质量,Oxlint 不负责 Vue 的完整类型推导;Vue S
## 接口基线 ## 接口基线
对齐后端 `qianlanse/short-drama-agent``dev` 对齐后端 `qianlanse/short-drama-agent``dev`
`c50d3f536fbca40ea07d36e00bace7bb886fabfa`2026-08-31),包含 Character Casting、身份/形态双参考首帧、首帧与视频生产就绪检查。本轮只修改前端,请确保后端依照其开发流程同步 Prisma migration 并重启服务。 `40c700f47d578888105484cfb317f52568855e0e`2026-09-01),包含 Character Casting、身份/形态双参考首帧、首帧与视频生产就绪检查,以及独立候选生成和中国人物默认选角基线。本轮只修改前端,请确保后端依照其开发流程同步 Prisma migration 并重启服务。
| HTTP | 路径(以 /api 为前缀) | 用途 | | HTTP | 路径(以 /api 为前缀) | 用途 |
| --- | --- | --- | | --- | --- | --- |
@@ -195,11 +195,12 @@ API 层也提供 `state` 与 `breakdown/latest` 方法。页面通过共享 chec
3. 前端以正式 `subject-forms` 目录为基础,并合并 Character Casting readiness,因此尚无形态的角色也可先完成 Identity 和选角。Scene / Prop 若还没有 Form,仍不会凭 checkpoint 伪造正式 Subject ID。 3. 前端以正式 `subject-forms` 目录为基础,并合并 Character Casting readiness,因此尚无形态的角色也可先完成 Identity 和选角。Scene / Prop 若还没有 Form,仍不会凭 checkpoint 伪造正式 Subject ID。
4. 身份文本批量分为 Character 专用入口和全部主体高级入口。选角候选批量只处理 `missing_anchor`,受本批上限控制;`candidate_pending` 必须人工确认,不会由批量任务静默决定演员。 4. 身份文本批量分为 Character 专用入口和全部主体高级入口。选角候选批量只处理 `missing_anchor`,受本批上限控制;`candidate_pending` 必须人工确认,不会由批量任务静默决定演员。
5. 身份图片的 `isAnchor` 来自专用 GET。Character primary 通过 Casting Candidate 接口生成且默认停用;点击确认演员后才成为 Anchor 并锁定 Identity。辅助视图不能直接成为母版。Scene / Prop 保留普通母版切换接口。 5. 身份图片的 `isAnchor` 来自专用 GET。Character primary 通过 Casting Candidate 接口生成且默认停用;点击确认演员后才成为 Anchor 并锁定 Identity。辅助视图不能直接成为母版。Scene / Prop 保留普通母版切换接口。
6. 身份生图不传 `referenceImageId` 时,由后端自动选择当前母版;无母版时按文本生成。指定时只能选择当前身份已完成的图片;候选图即使停用也可作为显式参考。后端没有“有母版但忽略它”的参数。尺寸成对留空或提供正整数。自定义 Prompt 会替换后端编译的完整文本,建议保留默认。 6. 普通身份生图不传 `referenceImageId` 时,由后端自动选择当前母版;无母版时按文本生成。指定时只能选择当前身份已完成的图片;候选图即使停用也可作为显式参考。Character Casting Candidate 是独立选角入口,即使已有 Anchor 也不会复用旧母版,避免旧演员身份污染新候选。尺寸成对留空或提供正整数。自定义 Prompt 会替换后端编译的完整文本,建议保留默认。
7. 目前**人物与场景形态生图**会自动注入当前身份母版,分别固定人物身份和空间骨架;道具暂不自动引用。没有身份或母版时,原形态生图流程仍可使用。已有母版但缺少 Provider 可访问远程地址时,后端会报错;本地 `/storage/` 能显示并不证明原始 Provider URL 仍可访问。前端显示后端错误,不自动重试消耗额度。 7. 目前**人物与场景形态生图**会自动注入当前身份母版,分别固定人物身份和空间骨架;道具暂不自动引用。没有身份或母版时,原形态生图流程仍可使用。已有母版但缺少 Provider 可访问远程地址时,后端会报错;本地 `/storage/` 能显示并不证明原始 Provider URL 仍可访问。前端显示后端错误,不自动重试消耗额度。
8. 修改风格、身份文本或母版,不会自动使旧图失效或重生成。已有形态主图仍会被分镜使用;需要保持新身份时,主动生成候选图并设为形态主图,再刷新分镜参考图,按需覆盖视频提示词。图库详情展示当次 `identityAnchorImageId``referenceImageId`;旧记录没有该字段时明确显示未知,不推断它使用了当前母版。 8. 修改风格、身份文本或母版,不会自动使旧图失效或重生成。已有形态主图仍会被分镜使用;需要保持新身份时,主动生成候选图并设为形态主图,再刷新分镜参考图,按需覆盖视频提示词。图库详情展示当次 `identityAnchorImageId``referenceImageId`;旧记录没有该字段时明确显示未知,不推断它使用了当前母版。
9. 风格参考图支持登记已有 HTTP(S)/`/storage/` 地址、分类、排序、启停和删除记录;**没有文件上传接口,也没有风格图 AI 生成接口**。当前身份图/形态图 Provider 调用不会自动消费风格参考图片,页面不暗示登记后已生效。 9. 风格参考图支持登记已有 HTTP(S)/`/storage/` 地址、分类、排序、启停和删除记录;**没有文件上传接口,也没有风格图 AI 生成接口**。当前身份图/形态图 Provider 调用不会自动消费风格参考图片,页面不暗示登记后已生效。
10. 编辑草稿与轮询分离。保存成功才清空草稿,失败保留;主体切换会提示放弃未保存修改。离开页面不会自动保存。所有新写操作沿用项目级会话互斥,生成前需要费用与后台任务确认,无法替代后端跨浏览器锁。 10. 编辑草稿与轮询分离。保存成功才清空草稿,失败保留;主体切换会提示放弃未保存修改。离开页面不会自动保存。所有新写操作沿用项目级会话互斥,生成前需要费用与后台任务确认,无法替代后端跨浏览器锁。
11. AI 生成 VisualStyle 与 Character Identity 时,若项目、世界观和角色事实都未指定人物背景,默认采用自然真实的中国人物选角基线。项目明确配置和角色事实优先于系统默认值,不限制后续使用其它国家、地区、族裔或肤色人物。
### 后端限制 ### 后端限制
@@ -130,7 +130,8 @@ function chooseAnchor(imageId: string) {
</div> </div>
<p class="alert mt-5 text-xs"> <p class="alert mt-5 text-xs">
Character 现在需要依次完成身份文本选角候选确认演员并锁定 Character 现在需要依次完成身份文本选角候选确认演员并锁定
Identity之后首帧才能就绪场景和道具仍使用普通身份母版流程旧形态图首帧与视频不会因母版切换自动更新 Identity之后首帧才能就绪未明确人物背景时AI
默认以中国人物作为选角基线项目和角色的明确事实始终优先场景和道具仍使用普通身份母版流程
</p> </p>
<p <p
v-if="catalog.error.value || styleQuery.error.value || castingQuery.error.value" v-if="catalog.error.value || styleQuery.error.value || castingQuery.error.value"
@@ -154,7 +155,8 @@ function chooseAnchor(imageId: string) {
<div> <div>
<h3 class="text-sm font-semibold">角色选角</h3> <h3 class="text-sm font-semibold">角色选角</h3>
<p class="mt-2 text-xs leading-6 text-muted"> <p class="mt-2 text-xs leading-6 text-muted">
只处理 Character候选图不会自动成为演员确认选择时才会同时切换 Anchor 并锁定 Identity 只处理 Character每张候选都从 Identity 独立生成不复用旧 Anchor确认选择时才会同时切换
Anchor 并锁定 Identity
</p> </p>
</div> </div>
</div> </div>
@@ -221,7 +223,7 @@ function chooseAnchor(imageId: string) {
castingBlocked || !hasStyle || !batchValid || !candidateLimitValid || !casting.missingAnchor castingBlocked || !hasStyle || !batchValid || !candidateLimitValid || !casting.missingAnchor
" "
acknowledgement acknowledgement
description="最多按本批上限为缺少 Anchor 的 Character 调用 Seedream,各生成一张停用的 primary 候选。已有候选、已有母版或已完成选角的角色不会重复处理。" description="最多按本批上限为缺少 Anchor 的 Character 调用 Seedream,各自独立生成一张停用的 primary 候选,不复用旧身份母版。已有候选、已有母版或已完成选角的角色不会重复处理。"
@confirm="generateCastingCandidates" @confirm="generateCastingCandidates"
/> />
</div> </div>
@@ -4,7 +4,7 @@ import { AppDialog } from '../../../components/ui'
import { validImageSize } from '../../subject-images/model' import { validImageSize } from '../../subject-images/model'
import type { GenerateCastingCandidateInput } from '../types' import type { GenerateCastingCandidateInput } from '../types'
/** Character 选角候选表单固定使用 primary,且不会自动选择演员。 */ /** Character 选角候选表单固定使用 primary,每次独立生成且不会自动选择演员。 */
const props = defineProps<{ subjectId: string; subjectName: string; disabled: boolean }>() const props = defineProps<{ subjectId: string; subjectName: string; disabled: boolean }>()
const open = defineModel<boolean>('open', { default: false }) const open = defineModel<boolean>('open', { default: false })
const emit = defineEmits<{ generate: [input: GenerateCastingCandidateInput] }>() const emit = defineEmits<{ generate: [input: GenerateCastingCandidateInput] }>()
@@ -39,8 +39,8 @@ function submit() {
<AppDialog v-model:open="open" title="生成角色选角候选" :description="`${subjectName} · 候选不会自动成为身份母版`"> <AppDialog v-model:open="open" title="生成角色选角候选" :description="`${subjectName} · 候选不会自动成为身份母版`">
<form class="mt-5 space-y-4" @submit.prevent="submit"> <form class="mt-5 space-y-4" @submit.prevent="submit">
<p class="alert text-xs"> <p class="alert text-xs">
后端会依据角色 Identity项目视觉风格和默认选角约束生成一张 primary 后端会依据角色 Identity项目视觉风格和默认选角约束独立生成一张 primary
候选生成完成后仍需人工点击确认选角 候选不会复用当前身份母版生成完成后仍需人工点击确认选角
</p> </p>
<label class="block"> <label class="block">
<span class="field-label">覆盖本次完整提示词可选</span> <span class="field-label">覆盖本次完整提示词可选</span>
@@ -48,7 +48,7 @@ function submit() {
id="casting-candidate-prompt" id="casting-candidate-prompt"
v-model="prompt" v-model="prompt"
class="input min-h-24" class="input min-h-24"
placeholder="建议留空,使用后端编译的稳定身份和选角约束" placeholder="建议留空,使用后端编译的稳定身份和选角约束;未明确人物背景时默认中国人物"
/> />
</label> </label>
<p v-if="prompt.trim()" class="text-xs text-danger"> <p v-if="prompt.trim()" class="text-xs text-danger">
@@ -199,6 +199,7 @@ describe('身份图与母版契约', () => {
props: { open: true, subjectId: 'subject-1', subjectName: '林夏', disabled: false } props: { open: true, subjectId: 'subject-1', subjectName: '林夏', disabled: false }
}) })
await flushPromises() await flushPromises()
expect(document.body.textContent).toContain('不会复用当前身份母版')
document.querySelector<HTMLInputElement>('#casting-candidate-cost')!.click() document.querySelector<HTMLInputElement>('#casting-candidate-cost')!.click()
await flushPromises() await flushPromises()
button('确认生成候选').click() button('确认生成候选').click()
+1 -1
View File
@@ -54,7 +54,7 @@ export interface GenerateIdentityImageInput {
height?: number height?: number
} }
/** Character 选角候选固定生成 primary不会自动成为 Anchor。 */ /** Character 选角候选固定独立生成 primary不复用旧 Anchor,也不会自动成为 Anchor。 */
export interface GenerateCastingCandidateInput { export interface GenerateCastingCandidateInput {
provider: 'seedream' provider: 'seedream'
prompt?: string prompt?: string
@@ -212,7 +212,7 @@ export function useSubjectIdentity() {
await detail.refresh() await detail.refresh()
} }
/** 单角色候选图固定为 primary,生成成功后仍需人工确认选角。 */ /** 单角色候选图固定为独立 primary不复用旧 Anchor生成成功后仍需人工确认选角。 */
async function generateCastingCandidate(input: GenerateCastingCandidateInput) { async function generateCastingCandidate(input: GenerateCastingCandidateInput) {
if (!canGenerateCandidate.value || !subject.value || !identity.value) return if (!canGenerateCandidate.value || !subject.value || !identity.value) return
const id = subject.value.id const id = subject.value.id
@@ -115,7 +115,8 @@ function removeImage(image: VisualStyleImage) {
有未保存修改AI 重生成暂不可用自动刷新不会覆盖编辑内容离开页面不会自动保存 有未保存修改AI 重生成暂不可用自动刷新不会覆盖编辑内容离开页面不会自动保存
</p> </p>
<p class="alert mb-5 text-xs"> <p class="alert mb-5 text-xs">
修改风格只影响之后的相关生成已有主体身份形态图片和视频提示词不会自动更新请按需要逐步重生成 AI
生成时若项目和世界观没有明确其它人物背景人物风格默认采用中国人物选角基线人工填写的人物背景和角色事实优先修改风格只影响之后的生成旧资产不会自动更新
</p> </p>
<StyleEditor <StyleEditor
v-if="query.data.value" v-if="query.data.value"
@@ -81,8 +81,12 @@ function save() {
</label> </label>
<div class="grid gap-5 lg:grid-cols-3"> <div class="grid gap-5 lg:grid-cols-3">
<label <label
><span class="field-label">人物风格补充</span ><span class="field-label">人物风格与选角背景</span
><textarea v-model="form.characterPrompt" class="input min-h-32" /> ><textarea
v-model="form.characterPrompt"
class="input min-h-32"
placeholder="人物质感、妆造与默认选角背景;留空时 AI 默认采用中国人物基线"
/>
</label> </label>
<label <label
><span class="field-label">场景风格补充</span ><span class="field-label">场景风格补充</span
+2
View File
@@ -245,6 +245,8 @@ describe('视觉风格与主体身份工作区', () => {
vi.stubGlobal('fetch', fetcher) vi.stubGlobal('fetch', fetcher)
await mountAssets('style') await mountAssets('style')
expect(wrapper!.text()).toContain('尚未创建') expect(wrapper!.text()).toContain('尚未创建')
expect(wrapper!.text()).toContain('默认采用中国人物选角基线')
expect(wrapper!.text()).toContain('人物风格与选角背景')
await wrapper!.get('#style-constraints').setValue('{"不应静默丢弃":true}') await wrapper!.get('#style-constraints').setValue('{"不应静默丢弃":true}')
expect(button('保存视觉风格').disabled).toBe(true) expect(button('保存视觉风格').disabled).toBe(true)
await wrapper!.get('#style-constraints').setValue('["真人写实"]') await wrapper!.get('#style-constraints').setValue('["真人写实"]')