feat: queue video generation per user
This commit is contained in:
@@ -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)}
|
||||
|
||||
Reference in New Issue
Block a user