feat: 补齐前端主流程并收紧工作台布局
统一工作台紧凑头部与形态图库间距,修复主体身份布局,并接入正式严格视频质量流水线。
This commit is contained in:
@@ -224,17 +224,11 @@ watch(
|
||||
>导演摄影设计
|
||||
</h3>
|
||||
<p class="mt-2 text-xs leading-6 text-muted">
|
||||
景别、机位、运镜与构图。单集操作会重新生成并覆盖;批量默认跳过完整剧集。
|
||||
景别、机位、运镜与构图。正式项目生成默认跳过已完整剧集,覆盖模式会重新生成全部剧集。
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<ConfirmAction
|
||||
label="生成本集导演设计"
|
||||
:disabled="!allowed['direction-one']"
|
||||
acknowledgement
|
||||
description="调用模型生成当前整集的导演设计,校验通过后保存。如果此集已有设计,将覆盖原结果。"
|
||||
@confirm="run('direction-one')"
|
||||
/><ConfirmAction
|
||||
:label="force ? '重生成全部导演设计' : '补齐项目导演设计'"
|
||||
:disabled="!allowed['direction-all']"
|
||||
acknowledgement
|
||||
@@ -259,12 +253,6 @@ watch(
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<ConfirmAction
|
||||
label="生成本集即时状态"
|
||||
:disabled="!allowed['visual-one']"
|
||||
acknowledgement
|
||||
description="依据本集导演设计和主体形态生成即时视觉状态,按配置尝试自动修复。校验通过才保存,已有状态将被覆盖。"
|
||||
@confirm="run('visual-one')"
|
||||
/><ConfirmAction
|
||||
:label="force ? '重生成全部即时状态' : '补齐项目即时状态'"
|
||||
:disabled="!allowed['visual-all']"
|
||||
acknowledgement
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { request } from '../../lib/http'
|
||||
import type {
|
||||
DirectionGeneration,
|
||||
EpisodeDirections,
|
||||
EpisodeVisualStates,
|
||||
PromptBatchResult,
|
||||
@@ -8,8 +7,7 @@ import type {
|
||||
ShotReferences,
|
||||
StoryboardBatchInput,
|
||||
StoryboardBatchResult,
|
||||
VisualStateBatchInput,
|
||||
VisualStateGeneration
|
||||
VisualStateBatchInput
|
||||
} from './types'
|
||||
import type { ShotGenerationSpec } from './generation-spec.types'
|
||||
|
||||
@@ -23,30 +21,18 @@ function shotPath(id: string) {
|
||||
return `/storyboard-shots/${encodeURIComponent(id)}`
|
||||
}
|
||||
|
||||
/** Storyboard 路由契约;单集入口虽名为 generate-test,但 persist=true 才适用于工作台。 */
|
||||
/** Storyboard 正式路由契约;测试路由不暴露为前端生产入口。 */
|
||||
export const storyboardApi = {
|
||||
directions: (id: string, episodeNo: number, signal?: AbortSignal) =>
|
||||
request<EpisodeDirections>(`${projectPath(id)}/storyboard-directions?episodeNo=${episodeNo}`, { signal }),
|
||||
visualStates: (id: string, episodeNo: number, signal?: AbortSignal) =>
|
||||
request<EpisodeVisualStates>(`${projectPath(id)}/storyboard-visual-states?episodeNo=${episodeNo}`, { signal }),
|
||||
generateDirection: (id: string, episodeNo: number) =>
|
||||
request<DirectionGeneration>(`${projectPath(id)}/storyboard-directions/generate-test`, {
|
||||
method: 'POST',
|
||||
body: { episodeNo, persist: true },
|
||||
timeoutMs: 0
|
||||
}),
|
||||
generateDirections: (id: string, input: StoryboardBatchInput) =>
|
||||
request<StoryboardBatchResult>(`${projectPath(id)}/storyboard-directions/generate`, {
|
||||
method: 'POST',
|
||||
body: input,
|
||||
timeoutMs: 0
|
||||
}),
|
||||
generateVisualState: (id: string, episodeNo: number, maxRepairAttempts: number) =>
|
||||
request<VisualStateGeneration>(`${projectPath(id)}/storyboard-visual-states/generate-test`, {
|
||||
method: 'POST',
|
||||
body: { episodeNo, persist: true, maxRepairAttempts },
|
||||
timeoutMs: 0
|
||||
}),
|
||||
generateVisualStates: (id: string, input: VisualStateBatchInput) =>
|
||||
request<StoryboardBatchResult>(`${projectPath(id)}/storyboard-visual-states/generate`, {
|
||||
method: 'POST',
|
||||
|
||||
@@ -3,24 +3,13 @@ import { NScrollbar, NAlert, NButton, NTable } from 'naive-ui'
|
||||
import { computed } from 'vue'
|
||||
import { Download } from '@lucide/vue'
|
||||
import { downloadText } from '../../../lib/format'
|
||||
import type { StoryboardIssue, StoryboardReceipt } from '../types'
|
||||
import type { StoryboardReceipt } from '../types'
|
||||
|
||||
/** 独立展示提交结果,不能将 HTTP 成功或局部成功显示为整批完成。 */
|
||||
const props = defineProps<{ receipt: StoryboardReceipt }>()
|
||||
const issues = computed<StoryboardIssue[]>(() => {
|
||||
if (props.receipt.kind === 'direction') return props.receipt.result.validation.issues
|
||||
if (props.receipt.kind === 'visual-state')
|
||||
return props.receipt.result.remainingIssues.length
|
||||
? props.receipt.result.remainingIssues
|
||||
: props.receipt.result.validation.issues
|
||||
return []
|
||||
})
|
||||
const failed = computed(() => {
|
||||
if (props.receipt.kind === 'batch') return props.receipt.result.failedEpisodes > 0
|
||||
if (props.receipt.kind === 'prompts') return props.receipt.result.failed > 0
|
||||
if (props.receipt.kind === 'visual-state' && !props.receipt.result.persisted) return true
|
||||
return !props.receipt.result.validation.valid
|
||||
})
|
||||
const failed = computed(() =>
|
||||
props.receipt.kind === 'batch' ? props.receipt.result.failedEpisodes > 0 : props.receipt.result.failed > 0
|
||||
)
|
||||
|
||||
/** 导出原始回执,保留后端提供的完整失败诊断。 */
|
||||
function exportReceipt() {
|
||||
@@ -91,24 +80,6 @@ function exportReceipt() {
|
||||
当前批量接口不返回成功镜头的 Prompt 正文;选择镜头后点击“读取/生成提示词”,已有内容会直接复用。
|
||||
</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p class="mt-4 text-sm" :class="failed ? 'text-danger' : 'text-success'">
|
||||
{{ receipt.result.validation.valid ? '生成校验通过' : '校验失败,未保存本次生成结果'
|
||||
}}<span v-if="receipt.kind === 'visual-state'">
|
||||
· 自动修复 {{ receipt.result.repairAttempts }} 次 ·
|
||||
{{ receipt.result.persisted ? '已持久化' : '未持久化' }}</span
|
||||
>
|
||||
</p>
|
||||
<p v-if="receipt.kind === 'direction' && receipt.result.validation.valid" class="mt-2 text-xs text-muted">
|
||||
已请求持久化,数据库实际覆盖率以下方刷新后的数据为准。
|
||||
</p>
|
||||
<ul class="mt-3 space-y-2 text-xs text-danger">
|
||||
<li v-for="(issue, index) in issues" :key="index">
|
||||
第 {{ issue.episodeNo }} 集 / Beat {{ issue.beatNo ?? '—' }} / Shot {{ issue.shotNo ?? '—' }}
|
||||
{{ issue.subjectRef }}:{{ issue.message }}
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
<p class="mt-4 text-[11px] text-muted">回执仅保留在当前浏览器会话中。数据库结果是覆盖率和镜头详情的依据。</p>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -23,7 +23,7 @@ export interface SubjectVisualState {
|
||||
transientState: string
|
||||
}
|
||||
|
||||
/** GET 返回扁平化的环境字段,与 generate-test 的嵌套 environment 不同。 */
|
||||
/** GET 返回已经持久化的扁平化环境字段。 */
|
||||
export interface ShotVisualState {
|
||||
timeOfDay: string
|
||||
weather: string
|
||||
@@ -62,33 +62,6 @@ export interface StoryboardIssue {
|
||||
message: string
|
||||
}
|
||||
|
||||
/** 单集生成结果中的校验信息,HTTP 200 不代表 valid。 */
|
||||
export interface StoryboardValidation {
|
||||
valid: boolean
|
||||
issues: StoryboardIssue[]
|
||||
}
|
||||
|
||||
/** 单集导演设计返回领域结构;是否落库应再查 GET 覆盖率确认。 */
|
||||
export interface DirectionGeneration {
|
||||
episodeNo: number
|
||||
direction: {
|
||||
episodeNo: number
|
||||
beatDirections: { beatNo: number; shots: (ShotDirection & { shotNo: number })[] }[]
|
||||
}
|
||||
validation: StoryboardValidation
|
||||
}
|
||||
|
||||
/** 单集 VisualState 的自动修复与落库回执。 */
|
||||
export interface VisualStateGeneration {
|
||||
episodeNo: number
|
||||
visualState: unknown
|
||||
validation: StoryboardValidation
|
||||
repaired: boolean
|
||||
repairAttempts: number
|
||||
remainingIssues: StoryboardIssue[]
|
||||
persisted: boolean
|
||||
}
|
||||
|
||||
/** 项目批量生成中的单集结果;保留失败原因和修复诊断。 */
|
||||
export interface EpisodeGenerationResult {
|
||||
episodeNo: number
|
||||
@@ -178,8 +151,6 @@ export interface DesignedShot {
|
||||
|
||||
/** 生成回执按操作区分,切换剧集时仍保留原始目标信息。 */
|
||||
export type StoryboardReceipt =
|
||||
| { kind: 'direction'; title: string; episodeNo: number; result: DirectionGeneration }
|
||||
| { kind: 'visual-state'; title: string; episodeNo: number; result: VisualStateGeneration }
|
||||
| { kind: 'batch'; title: string; result: StoryboardBatchResult }
|
||||
| { kind: 'prompts'; title: string; result: PromptBatchResult }
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ import { getOperation, runOperation } from '../workflows/operations'
|
||||
import { storyboardApi } from './api'
|
||||
import { getStoryboardSession, mergeDesignedShots, storyboardPrerequisites } from './model'
|
||||
|
||||
/** 分镜页允许的生成命令,保持单集与批量参数边界。 */
|
||||
export type StoryboardCommand = 'direction-one' | 'direction-all' | 'visual-one' | 'visual-all' | 'prompts'
|
||||
/** 分镜页只提供正式项目级生成命令,不暴露后端测试路由。 */
|
||||
export type StoryboardCommand = 'direction-all' | 'visual-all' | 'prompts'
|
||||
|
||||
/** 分镜查询、依赖判断与操作入口;不修改后端 graph。 */
|
||||
export function useStoryboard() {
|
||||
@@ -87,10 +87,7 @@ export function useStoryboard() {
|
||||
data.value.directions.coverage === 1
|
||||
)
|
||||
const allowed = computed<Record<StoryboardCommand, boolean>>(() => ({
|
||||
'direction-one': !blocked.value && prerequisites.value.directionEpisode && !!data.value?.directions.shotCount,
|
||||
'direction-all': !blocked.value && prerequisites.value.directionProject && batchValid.value,
|
||||
'visual-one':
|
||||
!blocked.value && prerequisites.value.visualEpisode && directionComplete.value && repairsValid.value,
|
||||
'visual-all': !blocked.value && prerequisites.value.visualProject && batchValid.value && repairsValid.value,
|
||||
prompts: !blocked.value && shots.value.length > 0 && batchValid.value
|
||||
}))
|
||||
@@ -99,28 +96,17 @@ export function useStoryboard() {
|
||||
async function run(command: StoryboardCommand) {
|
||||
if (!allowed.value[command]) return
|
||||
const projectId = id.value
|
||||
const number = episodeNo.value
|
||||
const batch = { concurrency: concurrency.value, force: force.value }
|
||||
const repairs = maxRepairAttempts.value
|
||||
const target = getStoryboardSession(projectId)
|
||||
const titles: Record<StoryboardCommand, string> = {
|
||||
'direction-one': `第 ${number} 集导演设计`,
|
||||
'direction-all': '项目导演设计',
|
||||
'visual-one': `第 ${number} 集即时状态`,
|
||||
'visual-all': '项目即时状态',
|
||||
prompts: '项目视频提示词'
|
||||
}
|
||||
target.receipt = null
|
||||
await runOperation(projectId, `生成${titles[command]}`, async () => {
|
||||
switch (command) {
|
||||
case 'direction-one':
|
||||
target.receipt = {
|
||||
kind: 'direction',
|
||||
title: titles[command],
|
||||
episodeNo: number,
|
||||
result: await storyboardApi.generateDirection(projectId, number)
|
||||
}
|
||||
break
|
||||
case 'direction-all':
|
||||
target.receipt = {
|
||||
kind: 'batch',
|
||||
@@ -128,14 +114,6 @@ export function useStoryboard() {
|
||||
result: await storyboardApi.generateDirections(projectId, batch)
|
||||
}
|
||||
break
|
||||
case 'visual-one':
|
||||
target.receipt = {
|
||||
kind: 'visual-state',
|
||||
title: titles[command],
|
||||
episodeNo: number,
|
||||
result: await storyboardApi.generateVisualState(projectId, number, repairs)
|
||||
}
|
||||
break
|
||||
case 'visual-all':
|
||||
target.receipt = {
|
||||
kind: 'batch',
|
||||
|
||||
Reference in New Issue
Block a user