feat: queue video generation per user

This commit is contained in:
2026-05-25 15:55:43 +08:00
parent f49d4b248c
commit 779e9b342b
7 changed files with 243 additions and 30 deletions

View File

@@ -96,6 +96,17 @@ function videoSrc(job: Job, video: GeneratedVideo) {
return apiAssetUrl(video.url || `/jobs/${job.id}/storyboard-videos/${video.id}.mp4`)
}
function videoStatusText(video: GeneratedVideo) {
if (video.status === "queued") {
return video.queue_message || (video.queue_position && video.queue_position > 1 ? `排队中 · 前方 ${video.queue_position - 1} 个任务` : "排队中 · 即将开始")
}
if (video.status === "in_progress") {
return video.queue_message ? `${video.queue_message} · ${Math.round(video.progress)}%` : `生成中 · ${Math.round(video.progress)}%`
}
if (video.status === "completed") return `${Math.round(video.duration)}s · 可播放`
return "失败 · 可重试"
}
function isVideoMode(mode: CreationMode) {
return mode !== "text-image"
}
@@ -307,7 +318,7 @@ export default function Home() {
model: videoModel,
})
setJob(updated)
toast.success("视频已提交")
toast.success("已加入生成队列")
} catch (e) {
const message = e instanceof Error ? e.message : "生视频失败"
setError(message)
@@ -523,21 +534,29 @@ export default function Home() {
{job ? <a href={`/detail/?job=${job.id}`} className="text-xs font-semibold text-cyan-200/82 hover:text-cyan-100"></a> : null}
</div>
{latestVideo && job ? (
<MediaAssetTile
kind="video"
src={latestVideo.status === "completed" ? videoSrc(job, latestVideo) : undefined}
poster={apiAssetUrl(latestVideo.poster_url)}
objectFit="cover"
previewObjectFit="contain"
className="aspect-video w-full rounded-xl"
videoControls={latestVideo.status === "completed"}
label={latestVideo.model}
meta={`${latestVideo.status} · ${Math.round(latestVideo.progress)}%`}
previewDetail={latestVideo.error || undefined}
emptyText={latestVideo.status === "failed" ? "失败" : undefined}
busy={latestVideo.status === "queued" || latestVideo.status === "in_progress"}
onDelete={() => deleteVideo(latestVideo)}
/>
<div className="grid gap-2">
{latestVideo.status === "queued" || latestVideo.status === "in_progress" ? (
<div className="flex items-center justify-between gap-3 rounded-xl border border-cyan-200/12 bg-cyan-200/8 px-3 py-2 text-xs text-cyan-50/78">
<span>{videoStatusText(latestVideo)}</span>
{latestVideo.status === "queued" && latestVideo.queue_size ? <span className="text-cyan-100/46"> {latestVideo.queue_position || 0}/{latestVideo.queue_size}</span> : null}
</div>
) : null}
<MediaAssetTile
kind="video"
src={latestVideo.status === "completed" ? videoSrc(job, latestVideo) : undefined}
poster={apiAssetUrl(latestVideo.poster_url)}
objectFit="cover"
previewObjectFit="contain"
className="aspect-video w-full rounded-xl"
videoControls={latestVideo.status === "completed"}
label={latestVideo.model}
meta={videoStatusText(latestVideo)}
previewDetail={latestVideo.error || undefined}
emptyText={latestVideo.status === "failed" ? "失败" : undefined}
busy={latestVideo.status === "queued" || latestVideo.status === "in_progress"}
onDelete={() => deleteVideo(latestVideo)}
/>
</div>
) : latestImage ? (
<MediaAssetTile
src={apiAssetUrl(latestImage.url)}