fix: align generation size and duration options

This commit is contained in:
2026-05-25 14:23:09 +08:00
parent fa64f95911
commit e77e77fada
5 changed files with 239 additions and 23 deletions

View File

@@ -29,6 +29,7 @@ import {
type GeneratedVideo,
type Job,
type RuntimeModelOption,
type RuntimeSizeOption,
} from "@/lib/api"
type CreationMode = "text-video" | "text-image" | "first-frame-video" | "first-last-frame-video"
@@ -99,6 +100,19 @@ function isVideoMode(mode: CreationMode) {
return mode !== "text-image"
}
const DEFAULT_IMAGE_SIZE_OPTIONS: RuntimeSizeOption[] = [
{ id: "1024x1536", label: "竖图 2:3", value: "1024x1536" },
{ id: "1024x1024", label: "方图 1:1", value: "1024x1024" },
{ id: "1536x1024", label: "横图 3:2", value: "1536x1024" },
{ id: "auto", label: "自动", value: "auto" },
]
const DEFAULT_VIDEO_SIZE_OPTIONS: RuntimeSizeOption[] = [
{ id: "720x1280", label: "竖屏 9:16", value: "720x1280" },
{ id: "1280x720", label: "横屏 16:9", value: "1280x720" },
{ id: "1024x1024", label: "方形 1:1", value: "1024x1024" },
]
export default function Home() {
const [mode, setMode] = useState<CreationMode>("text-video")
const [prompt, setPrompt] = useState("")
@@ -109,12 +123,17 @@ export default function Home() {
const [lastFramePreview, setLastFramePreview] = useState("")
const [imageModel, setImageModel] = useState("auto")
const [videoModel, setVideoModel] = useState("seedance")
const [imageSize, setImageSize] = useState("1024x1536")
const [videoSize, setVideoSize] = useState("720x1280")
const [videoDurationOptions, setVideoDurationOptions] = useState<number[]>([5, 8, 10, 12, 15])
const [imageOptions, setImageOptions] = useState<RuntimeModelOption[]>([
{ id: "auto", label: "自动", model: "gpt-image-2", available: true },
])
const [videoOptions, setVideoOptions] = useState<RuntimeModelOption[]>([
{ id: "seedance", label: "Seedance", model: "seedance", available: true },
])
const [imageSizeOptions, setImageSizeOptions] = useState<RuntimeSizeOption[]>(DEFAULT_IMAGE_SIZE_OPTIONS)
const [videoSizeOptions, setVideoSizeOptions] = useState<RuntimeSizeOption[]>(DEFAULT_VIDEO_SIZE_OPTIONS)
const [job, setJob] = useState<Job | null>(null)
const [busy, setBusy] = useState<BusyTask>(null)
const [error, setError] = useState("")
@@ -140,14 +159,26 @@ export default function Home() {
const nextVideoOptions = models?.video_options?.length
? models.video_options
: [{ id: models?.video || "seedance", label: "Seedance", model: models?.video || "seedance", available: !!models?.video_configured }]
const nextImageSizeOptions = models?.image_size_options?.length ? models.image_size_options : DEFAULT_IMAGE_SIZE_OPTIONS
const nextVideoSizeOptions = models?.video_size_options?.length ? models.video_size_options : DEFAULT_VIDEO_SIZE_OPTIONS
const nextDurationOptions = models?.video_duration_options?.length ? models.video_duration_options : [5, 8, 10, 12, 15]
setImageOptions(nextImageOptions)
setVideoOptions(nextVideoOptions)
setImageSizeOptions(nextImageSizeOptions)
setVideoSizeOptions(nextVideoSizeOptions)
setVideoDurationOptions(nextDurationOptions)
if (!nextImageOptions.some((item) => item.id === imageModel)) setImageModel(nextImageOptions[0]?.id || "auto")
if (!nextVideoOptions.some((item) => item.id === videoModel)) setVideoModel(nextVideoOptions[0]?.id || "seedance")
if (!nextImageSizeOptions.some((item) => item.value === imageSize)) setImageSize(nextImageSizeOptions[0]?.value || "1024x1536")
if (!nextVideoSizeOptions.some((item) => item.value === videoSize)) setVideoSize(nextVideoSizeOptions[0]?.value || "720x1280")
if (!nextDurationOptions.includes(seconds)) setSeconds(nextDurationOptions.includes(12) ? 12 : (nextDurationOptions[0] ?? 5))
})
.catch(() => {
setImageOptions([{ id: "auto", label: "自动", model: "gpt-image-2", available: true }])
setVideoOptions([{ id: "seedance", label: "Seedance", model: "seedance", available: true }])
setImageSizeOptions(DEFAULT_IMAGE_SIZE_OPTIONS)
setVideoSizeOptions(DEFAULT_VIDEO_SIZE_OPTIONS)
setVideoDurationOptions([5, 8, 10, 12, 15])
})
}, [])
@@ -246,6 +277,7 @@ export default function Home() {
prompt: promptWithGuardrails(),
mode: "text",
model: imageModel,
size: imageSize,
})
setJob(updated)
toast.success("图片已生成")
@@ -271,7 +303,7 @@ export default function Home() {
count: 1,
first_image: activeMode.needsFirstFrame ? { kind: "keyframe", frame_idx: 0 } : null,
last_image: activeMode.needsLastFrame && lastFrame ? { kind: "keyframe", frame_idx: lastFrame.index } : null,
size: "720x1280",
size: videoSize,
model: videoModel,
})
setJob(updated)
@@ -423,17 +455,48 @@ export default function Home() {
</select>
</label>
{isVideoMode(mode) ? (
<>
<label className="inline-flex h-9 items-center gap-2 rounded-xl border border-white/7 bg-black/14 px-3">
<select
value={videoSize}
onChange={(event) => setVideoSize(event.target.value)}
className="max-w-32 bg-transparent text-white/76 outline-none"
>
{videoSizeOptions.map((item) => (
<option key={item.value} value={item.value}>
{item.label}
</option>
))}
</select>
</label>
<label className="inline-flex h-9 items-center gap-2 rounded-xl border border-white/7 bg-black/14 px-3">
<select
value={seconds}
onChange={(event) => setSeconds(Number(event.target.value))}
className="bg-transparent text-white/76 outline-none"
>
{videoDurationOptions.map((value) => <option key={value} value={value}>{value}s</option>)}
</select>
</label>
</>
) : (
<label className="inline-flex h-9 items-center gap-2 rounded-xl border border-white/7 bg-black/14 px-3">
<select
value={seconds}
onChange={(event) => setSeconds(Number(event.target.value))}
className="bg-transparent text-white/76 outline-none"
value={imageSize}
onChange={(event) => setImageSize(event.target.value)}
className="max-w-32 bg-transparent text-white/76 outline-none"
>
{[5, 8, 12, 15, 20, 30].map((value) => <option key={value} value={value}>{value}s</option>)}
{imageSizeOptions.map((item) => (
<option key={item.value} value={item.value}>
{item.label}
</option>
))}
</select>
</label>
) : null}
)}
<span>{activeMode.needsFirstFrame ? "图片作为参考帧" : "只根据文字生成"}</span>
</div>

View File

@@ -260,6 +260,16 @@ export interface RuntimeModelOption {
model: string
description?: string
available?: boolean
duration_options?: number[]
max_duration_seconds?: number
size_options?: RuntimeSizeOption[]
}
export interface RuntimeSizeOption {
id: string
label: string
value: string
description?: string
}
export interface RuntimeModels {
@@ -280,6 +290,7 @@ export interface RuntimeModels {
image?: string
image_base_url?: string
image_options?: RuntimeModelOption[]
image_size_options?: RuntimeSizeOption[]
image_fallbacks?: string[]
image_circuit?: {
primary?: string
@@ -303,6 +314,9 @@ export interface RuntimeModels {
video?: string
video_aliases?: Record<string, string>
video_options?: RuntimeModelOption[]
video_duration_options?: number[]
video_max_duration_seconds?: number
video_size_options?: RuntimeSizeOption[]
video_provider?: string
video_base_url?: string
video_configured?: boolean
@@ -1231,7 +1245,7 @@ export async function translateText(text: string, target: "en" | "zh" = "en"): P
export async function generateImage(
jobId: string,
frameIdx: number,
body: { prompt: string; extra_prompt?: string; negative_prompt?: string; model?: string; mode?: "edit" | "text"; from_selected?: boolean },
body: { prompt: string; extra_prompt?: string; negative_prompt?: string; model?: string; size?: string; mode?: "edit" | "text"; from_selected?: boolean },
): Promise<Job> {
const res = await fetch(`${API_BASE}/jobs/${jobId}/frames/${frameIdx}/generate`, {
method: "POST",