Files
short-drama-agent-front/src/features/production/useProduction.ts
T

144 lines
6.6 KiB
TypeScript

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
}
}