feat: 同步镜头首帧与视频生产工作区
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import { usePolling } from '../../composables/usePolling'
|
||||
import { useProjectContext } from '../projects/context'
|
||||
import { mergeDesignedShots } from '../storyboard/model'
|
||||
import { storyboardApi } from '../storyboard/api'
|
||||
import { breakdownSnapshot } from '../workflows/selectors'
|
||||
import { getOperation, runOperation } from '../workflows/operations'
|
||||
import { productionApi } from './api'
|
||||
import { getProductionSession } from './model'
|
||||
|
||||
/** 项目级生产动作;每次请求都由统一项目锁防止重复提交。 */
|
||||
export type ProductionCommand = 'prompts' | 'keyframes' | 'videos' | 'retry-videos'
|
||||
|
||||
/** 镜头生产页的查询、筛选、就绪状态与批量操作。 */
|
||||
export function useProduction() {
|
||||
const context = useProjectContext()
|
||||
const id = computed(() => context.project.value?.id ?? '')
|
||||
const selectedEpisode = ref<number>()
|
||||
const selectedShot = ref('')
|
||||
const concurrency = ref(2)
|
||||
const force = ref(false)
|
||||
const snapshot = computed(() => breakdownSnapshot(context.checkpoints.value))
|
||||
const sourceEpisodes = computed(
|
||||
() => snapshot.value?.storyboardEpisodeShots ?? snapshot.value?.breakdownResult?.storyboardEpisodeShots ?? []
|
||||
)
|
||||
const episodeOptions = computed(() => {
|
||||
const entries = new Map(context.project.value?.episodes.map(episode => [episode.episode, episode.title]) ?? [])
|
||||
for (const episode of sourceEpisodes.value) {
|
||||
if (!entries.has(episode.episodeNo)) entries.set(episode.episodeNo, episode.episodePlan.episodeTitle)
|
||||
}
|
||||
return [...entries]
|
||||
.map(([episodeNo, title]) => ({ episodeNo, title }))
|
||||
.toSorted((a, b) => a.episodeNo - b.episodeNo)
|
||||
})
|
||||
const episodeNo = computed(
|
||||
() =>
|
||||
episodeOptions.value.find(item => item.episodeNo === selectedEpisode.value)?.episodeNo ??
|
||||
episodeOptions.value[0]?.episodeNo ??
|
||||
0
|
||||
)
|
||||
const queryKey = computed(() => JSON.stringify([id.value, episodeNo.value, force.value]))
|
||||
const query = usePolling(queryKey, async (key, signal) => {
|
||||
const [projectId, number, overwrite] = JSON.parse(key) as [string, number, boolean]
|
||||
if (!projectId || !number) return null
|
||||
const [directions, prompts, keyframes, videos, videoStatus] = await Promise.all([
|
||||
storyboardApi.directions(projectId, number, signal),
|
||||
productionApi.promptReadiness(projectId, overwrite, signal),
|
||||
productionApi.keyframeReadiness(projectId, overwrite, signal),
|
||||
productionApi.videoReadiness(projectId, overwrite, signal),
|
||||
productionApi.projectVideoStatus(projectId, signal)
|
||||
])
|
||||
if (directions.projectId !== projectId || directions.episodeNo !== number)
|
||||
throw new Error('镜头生产查询返回了不匹配的项目或剧集,请刷新后重试。')
|
||||
return { directions, prompts, keyframes, videos, videoStatus }
|
||||
})
|
||||
const source = computed(() => sourceEpisodes.value.find(item => item.episodeNo === episodeNo.value))
|
||||
const shots = computed(() => mergeDesignedShots(query.data.value?.directions ?? null, null, source.value))
|
||||
const shot = computed(() => shots.value.find(item => item.shotId === selectedShot.value) ?? shots.value[0])
|
||||
const promptItem = computed(() => query.data.value?.prompts.items.find(item => item.shotId === shot.value?.shotId))
|
||||
const keyframeItem = computed(() =>
|
||||
query.data.value?.keyframes.items.find(item => item.shotId === shot.value?.shotId)
|
||||
)
|
||||
const videoItem = computed(() => query.data.value?.videos.items.find(item => item.shotId === shot.value?.shotId))
|
||||
const videoStatusItem = computed(() =>
|
||||
query.data.value?.videoStatus.items.find(item => item.shotId === shot.value?.shotId)
|
||||
)
|
||||
const operation = computed(() => getOperation(id.value))
|
||||
const session = computed(() => getProductionSession(id.value))
|
||||
const batchValid = computed(() => Number.isSafeInteger(concurrency.value) && concurrency.value > 0)
|
||||
const blocked = computed(
|
||||
() =>
|
||||
operation.value.pending ||
|
||||
!!context.error.value ||
|
||||
!!query.error.value ||
|
||||
!query.data.value ||
|
||||
context.project.value?.status === 'generating'
|
||||
)
|
||||
|
||||
/** 批量操作只向就绪镜头提交;最终资产状态由轮询查询确认。 */
|
||||
async function run(command: ProductionCommand) {
|
||||
if (blocked.value || !batchValid.value) return
|
||||
const projectId = id.value
|
||||
const input = { concurrency: concurrency.value, force: force.value }
|
||||
const target = getProductionSession(projectId)
|
||||
target.receipt = null
|
||||
const labels: Record<ProductionCommand, string> = {
|
||||
prompts: force.value ? '重生成项目视频提示词' : '补齐项目视频提示词',
|
||||
keyframes: force.value ? '新增项目首帧候选' : '补齐项目主首帧',
|
||||
videos: force.value ? '新增项目视频候选' : '提交就绪视频任务',
|
||||
'retry-videos': '重试失败视频任务'
|
||||
}
|
||||
await runOperation(projectId, labels[command], async () => {
|
||||
if (command === 'prompts') {
|
||||
target.receipt = {
|
||||
kind: 'prompts',
|
||||
title: labels[command],
|
||||
result: await productionApi.generatePrompts(projectId, input)
|
||||
}
|
||||
} else if (command === 'keyframes') {
|
||||
target.receipt = {
|
||||
kind: 'keyframes',
|
||||
title: labels[command],
|
||||
result: await productionApi.generateKeyframes(projectId, { provider: 'seedream', ...input })
|
||||
}
|
||||
} else if (command === 'videos') {
|
||||
target.receipt = {
|
||||
kind: 'videos',
|
||||
title: labels[command],
|
||||
result: await productionApi.generateVideos(projectId, { provider: 'seedance', ...input })
|
||||
}
|
||||
} else {
|
||||
target.receipt = {
|
||||
kind: 'retry',
|
||||
title: labels[command],
|
||||
result: await productionApi.retryVideos(projectId, concurrency.value)
|
||||
}
|
||||
}
|
||||
})
|
||||
await query.refresh()
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
selectedEpisode,
|
||||
selectedShot,
|
||||
episodeNo,
|
||||
episodeOptions,
|
||||
shots,
|
||||
shot,
|
||||
promptItem,
|
||||
keyframeItem,
|
||||
videoItem,
|
||||
videoStatusItem,
|
||||
query,
|
||||
operation,
|
||||
session,
|
||||
concurrency,
|
||||
force,
|
||||
batchValid,
|
||||
blocked,
|
||||
run
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user