feat(canvas): 视频参数 - 支持自定义时长并统一分辨率与设置面板
This commit is contained in:
1 parent
7dd2a5881b
commit
98aad91d5f
8 files changed
+63
-51
No files matched your search
@@ -1044,11 +1044,6 @@ func newAPIChannel2VideoBody(input canvasGenerationInput) (map[string]interface{
|
|||||||
if secondsErr != nil || seconds < 1 {
|
if secondsErr != nil || seconds < 1 {
|
||||||
seconds = 6
|
seconds = 6
|
||||||
}
|
}
|
||||||
if len(images) > 1 && seconds > 10 {
|
|
||||||
seconds = 10
|
|
||||||
} else if seconds > 15 {
|
|
||||||
seconds = 15
|
|
||||||
}
|
|
||||||
ratio := normalizeNewAPIChannel2Ratio(input.Config.Size, modelName)
|
ratio := normalizeNewAPIChannel2Ratio(input.Config.Size, modelName)
|
||||||
resolution := normalizeNewAPIChannel2Resolution(input.Config.VQuality, modelName)
|
resolution := normalizeNewAPIChannel2Resolution(input.Config.VQuality, modelName)
|
||||||
body := map[string]interface{}{
|
body := map[string]interface{}{
|
||||||
@@ -2170,9 +2165,6 @@ func normalizeXAIVideoDuration(value string) int {
|
|||||||
if err != nil || duration <= 0 {
|
if err != nil || duration <= 0 {
|
||||||
return 6
|
return 6
|
||||||
}
|
}
|
||||||
if duration > 15 {
|
|
||||||
return 15
|
|
||||||
}
|
|
||||||
return duration
|
return duration
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2238,24 +2230,14 @@ func normalizeSeedanceDuration(value string) int {
|
|||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
seconds, err := strconv.Atoi(strings.TrimSpace(value))
|
seconds, err := strconv.Atoi(strings.TrimSpace(value))
|
||||||
if err != nil || seconds == 0 {
|
if err != nil || seconds <= 0 {
|
||||||
seconds = 5
|
return 5
|
||||||
}
|
|
||||||
if seconds < 4 {
|
|
||||||
return 4
|
|
||||||
}
|
|
||||||
if seconds > 15 {
|
|
||||||
return 15
|
|
||||||
}
|
}
|
||||||
return seconds
|
return seconds
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeSeedanceVideosDuration(value string) int {
|
func normalizeSeedanceVideosDuration(value string) int {
|
||||||
seconds := normalizeSeedanceDuration(value)
|
return normalizeSeedanceDuration(value)
|
||||||
if seconds < 4 {
|
|
||||||
return 5
|
|
||||||
}
|
|
||||||
return seconds
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeSeedanceRatio(value string) string {
|
func normalizeSeedanceRatio(value string) string {
|
||||||
|
|||||||
@@ -527,7 +527,7 @@ func TestXAIVideoBodyUsesOfficialImageShapeAndNormalizesSettings(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("grokVideoBody() error = %v", err)
|
t.Fatalf("grokVideoBody() error = %v", err)
|
||||||
}
|
}
|
||||||
if body["duration"] != 15 || body["aspect_ratio"] != "9:16" || body["resolution"] != "1080p" {
|
if body["duration"] != 20 || body["aspect_ratio"] != "9:16" || body["resolution"] != "1080p" {
|
||||||
t.Fatalf("xAI settings = %#v", body)
|
t.Fatalf("xAI settings = %#v", body)
|
||||||
}
|
}
|
||||||
image, ok := body["image"].(map[string]interface{})
|
image, ok := body["image"].(map[string]interface{})
|
||||||
|
|||||||
@@ -103,10 +103,10 @@ function ImageSettingsPortal({
|
|||||||
width,
|
width,
|
||||||
left: Math.max(margin, Math.min(window.innerWidth - width - margin, left)),
|
left: Math.max(margin, Math.min(window.innerWidth - width - margin, left)),
|
||||||
...(topPlacement ? { bottom: window.innerHeight - buttonRect.top + gap, maxHeight: Math.max(260, buttonRect.top - margin * 2) } : { top: buttonRect.bottom + gap, maxHeight: Math.max(260, window.innerHeight - buttonRect.bottom - margin * 2) }),
|
...(topPlacement ? { bottom: window.innerHeight - buttonRect.top + gap, maxHeight: Math.max(260, buttonRect.top - margin * 2) } : { top: buttonRect.bottom + gap, maxHeight: Math.max(260, window.innerHeight - buttonRect.bottom - margin * 2) }),
|
||||||
background: theme.spatial.elevated,
|
background: theme.canvas.background,
|
||||||
border: `1px solid ${theme.toolbar.border}`,
|
border: `1px solid ${theme.toolbar.border}`,
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
boxShadow: `0 24px 72px ${theme.spatial.shadow}, inset 0 1px 0 rgba(255,255,255,.08)`,
|
boxShadow: `0 24px 72px ${theme.spatial.shadow}`,
|
||||||
padding: 12,
|
padding: 12,
|
||||||
overflowY: "auto",
|
overflowY: "auto",
|
||||||
color: theme.node.text,
|
color: theme.node.text,
|
||||||
|
|||||||
@@ -91,10 +91,10 @@ function VideoSettingsPortal({
|
|||||||
width,
|
width,
|
||||||
left: Math.max(margin, Math.min(window.innerWidth - width - margin, left)),
|
left: Math.max(margin, Math.min(window.innerWidth - width - margin, left)),
|
||||||
...(placeAbove ? { bottom: window.innerHeight - buttonRect.top + gap, maxHeight: Math.max(260, topSpace) } : { top: buttonRect.bottom + gap, maxHeight: Math.max(260, bottomSpace) }),
|
...(placeAbove ? { bottom: window.innerHeight - buttonRect.top + gap, maxHeight: Math.max(260, topSpace) } : { top: buttonRect.bottom + gap, maxHeight: Math.max(260, bottomSpace) }),
|
||||||
background: theme.spatial.elevated,
|
background: theme.canvas.background,
|
||||||
border: `1px solid ${theme.toolbar.border}`,
|
border: `1px solid ${theme.toolbar.border}`,
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
boxShadow: `0 24px 72px ${theme.spatial.shadow}, inset 0 1px 0 rgba(255,255,255,.08)`,
|
boxShadow: `0 24px 72px ${theme.spatial.shadow}`,
|
||||||
padding: 12,
|
padding: 12,
|
||||||
overflowY: "auto",
|
overflowY: "auto",
|
||||||
color: theme.node.text,
|
color: theme.node.text,
|
||||||
|
|||||||
@@ -121,8 +121,8 @@ export function ImageSettingsPanel({ config, onConfigChange, theme, showTitle =
|
|||||||
<button
|
<button
|
||||||
key={item.value}
|
key={item.value}
|
||||||
type="button"
|
type="button"
|
||||||
className="flex h-[58px] cursor-pointer flex-col items-center justify-center gap-0.5 rounded-lg border bg-transparent text-xs transition hover:opacity-80"
|
className="flex h-[58px] cursor-pointer flex-col items-center justify-center gap-0.5 rounded-lg bg-transparent text-xs transition-colors hover:brightness-110 focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-1"
|
||||||
style={{ borderColor: selectedAspect?.value === item.value ? theme.node.text : theme.node.stroke, background: "transparent", color: theme.node.text }}
|
style={{ background: selectedAspect?.value === item.value ? theme.toolbar.activeBg : "transparent", color: theme.node.text, outlineColor: theme.node.muted }}
|
||||||
onMouseDown={(event) => event.stopPropagation()}
|
onMouseDown={(event) => event.stopPropagation()}
|
||||||
onClick={() => selectAspect(item.value)}
|
onClick={() => selectAspect(item.value)}
|
||||||
>
|
>
|
||||||
@@ -154,8 +154,8 @@ export function ImageSettingsTheme({ theme, children }: { theme: CanvasTheme; ch
|
|||||||
return (
|
return (
|
||||||
<ConfigProvider
|
<ConfigProvider
|
||||||
theme={{
|
theme={{
|
||||||
token: { colorBgContainer: theme.toolbar.panel, colorBgElevated: theme.toolbar.panel, colorBorder: theme.node.stroke, colorPrimary: theme.node.activeStroke, colorText: theme.node.text, colorTextLightSolid: theme.node.panel },
|
token: { colorBgContainer: theme.canvas.background, colorBgElevated: theme.canvas.background, colorBorder: theme.node.stroke, colorPrimary: theme.node.activeStroke, colorText: theme.node.text, colorTextLightSolid: theme.node.panel },
|
||||||
components: { Button: { defaultBg: theme.toolbar.panel, defaultBorderColor: theme.node.stroke, defaultColor: theme.node.text } },
|
components: { Button: { defaultBg: theme.canvas.background, defaultBorderColor: theme.node.stroke, defaultColor: theme.node.text } },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -175,8 +175,8 @@ function OptionPill({ selected, theme, onClick, children }: { selected: boolean;
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="h-8 cursor-pointer rounded-full border px-2 text-xs transition hover:opacity-80"
|
className="h-8 cursor-pointer rounded-full px-2 text-xs transition-colors hover:brightness-110 focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-1"
|
||||||
style={{ background: "transparent", borderColor: selected ? theme.node.text : theme.node.stroke, color: theme.node.text }}
|
style={{ background: selected ? theme.toolbar.activeBg : "transparent", color: theme.node.text, outlineColor: theme.node.muted }}
|
||||||
onMouseDown={(event) => event.stopPropagation()}
|
onMouseDown={(event) => event.stopPropagation()}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
@@ -193,7 +193,7 @@ function DimensionInput({ prefix, value, disabled, theme, alignToStep, onChange
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label className="flex h-8 overflow-hidden rounded-lg text-xs" style={{ background: theme.node.fill, color: theme.node.text, opacity: disabled ? 0.55 : 1 }}>
|
<label className="flex h-8 overflow-hidden rounded-lg text-xs" style={{ background: theme.toolbar.itemHover, color: theme.node.text, opacity: disabled ? 0.55 : 1 }}>
|
||||||
<span className="grid w-8 place-items-center" style={{ color: theme.node.muted }}>
|
<span className="grid w-8 place-items-center" style={{ color: theme.node.muted }}>
|
||||||
{prefix}
|
{prefix}
|
||||||
</span>
|
</span>
|
||||||
@@ -216,7 +216,7 @@ function DimensionInput({ prefix, value, disabled, theme, alignToStep, onChange
|
|||||||
|
|
||||||
function CountInput({ value, max, theme, onChange }: { value: number; max: number; theme: CanvasTheme; onChange: (value: number | null) => void }) {
|
function CountInput({ value, max, theme, onChange }: { value: number; max: number; theme: CanvasTheme; onChange: (value: number | null) => void }) {
|
||||||
return (
|
return (
|
||||||
<label className="col-span-2 flex h-8 overflow-hidden rounded-full border text-xs" style={{ borderColor: theme.node.stroke, color: theme.node.text }}>
|
<label className="col-span-2 flex h-8 overflow-hidden rounded-full text-xs" style={{ background: theme.toolbar.itemHover, color: theme.node.text }}>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min={1}
|
min={1}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Switch } from "antd";
|
|||||||
import { ImageSettingsTheme } from "@/components/image-settings-panel";
|
import { ImageSettingsTheme } from "@/components/image-settings-panel";
|
||||||
import { boolConfig, isSeedanceFastModel, isSeedanceVideoConfig, normalizeSeedanceDuration, normalizeSeedanceRatio, normalizeSeedanceResolution, seedanceDurationOptions, seedanceRatioOptions, seedanceResolutionOptions } from "@/lib/seedance-video";
|
import { boolConfig, isSeedanceFastModel, isSeedanceVideoConfig, normalizeSeedanceDuration, normalizeSeedanceRatio, normalizeSeedanceResolution, seedanceDurationOptions, seedanceRatioOptions, seedanceResolutionOptions } from "@/lib/seedance-video";
|
||||||
import { type CanvasTheme } from "@/lib/canvas-theme";
|
import { type CanvasTheme } from "@/lib/canvas-theme";
|
||||||
import { normalizeVideoDuration, normalizeVideoResolution, VIDEO_DURATION_OPTIONS, VIDEO_RESOLUTION_OPTIONS } from "@/lib/video-generation-options";
|
import { normalizeVideoDuration, normalizeVideoResolution, VIDEO_DURATION_MIN, VIDEO_DURATION_OPTIONS, VIDEO_RESOLUTION_OPTIONS } from "@/lib/video-generation-options";
|
||||||
import { modelOptionName, resolveModelRequestConfig, type AiConfig } from "@/stores/use-config-store";
|
import { modelOptionName, resolveModelRequestConfig, type AiConfig } from "@/stores/use-config-store";
|
||||||
|
|
||||||
const resolutionOptions = VIDEO_RESOLUTION_OPTIONS.map((value) => ({ value: String(value), label: `${value}P` }));
|
const resolutionOptions = VIDEO_RESOLUTION_OPTIONS.map((value) => ({ value: String(value), label: `${value}P` }));
|
||||||
@@ -51,7 +51,7 @@ export function VideoSettingsPanel({ config, onConfigChange, theme, showTitle =
|
|||||||
<ImageSettingsTheme theme={theme}>
|
<ImageSettingsTheme theme={theme}>
|
||||||
<div className={className} style={{ color: theme.node.text }} onMouseDown={(event) => event.stopPropagation()}>
|
<div className={className} style={{ color: theme.node.text }} onMouseDown={(event) => event.stopPropagation()}>
|
||||||
{showTitle ? <div className="text-sm font-semibold">视频设置</div> : null}
|
{showTitle ? <div className="text-sm font-semibold">视频设置</div> : null}
|
||||||
<SettingGroup title="清晰度" color={theme.node.muted}>
|
<SettingGroup title="分辨率" color={theme.node.muted}>
|
||||||
<div className="grid grid-cols-3 gap-1.5">
|
<div className="grid grid-cols-3 gap-1.5">
|
||||||
{resolutionOptions.map((item) => (
|
{resolutionOptions.map((item) => (
|
||||||
<OptionPill key={item.value} selected={resolution === item.value} theme={theme} onClick={() => onConfigChange("vquality", item.value)}>
|
<OptionPill key={item.value} selected={resolution === item.value} theme={theme} onClick={() => onConfigChange("vquality", item.value)}>
|
||||||
@@ -71,24 +71,25 @@ export function VideoSettingsPanel({ config, onConfigChange, theme, showTitle =
|
|||||||
<button
|
<button
|
||||||
key={item.value}
|
key={item.value}
|
||||||
type="button"
|
type="button"
|
||||||
className="flex h-8 cursor-pointer items-center justify-center gap-1.5 rounded-md border px-1 text-[11px] font-medium transition hover:opacity-80"
|
className="flex h-8 cursor-pointer items-center justify-center gap-1.5 rounded-md px-1 text-[11px] font-medium transition-colors hover:brightness-110 focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-1"
|
||||||
style={{ background: size === item.value ? theme.accent.primarySoft : "transparent", borderColor: size === item.value ? theme.accent.primary : theme.node.stroke, color: size === item.value ? theme.accent.primary : theme.node.text }}
|
style={{ background: size === item.value ? theme.toolbar.activeBg : "transparent", color: theme.node.text, outlineColor: theme.node.muted }}
|
||||||
onMouseDown={(event) => event.stopPropagation()}
|
onMouseDown={(event) => event.stopPropagation()}
|
||||||
onClick={() => onConfigChange("size", item.value)}
|
onClick={() => onConfigChange("size", item.value)}
|
||||||
>
|
>
|
||||||
<SizePreview width={item.width} height={item.height} color={size === item.value ? theme.accent.primary : theme.node.text} />
|
<SizePreview width={item.width} height={item.height} color={theme.node.text} />
|
||||||
<span>{item.label}</span>
|
<span>{item.label}</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</SettingGroup>
|
</SettingGroup>
|
||||||
<SettingGroup title="秒数" color={theme.node.muted}>
|
<SettingGroup title="秒数" color={theme.node.muted}>
|
||||||
<div className="grid gap-1.5" style={{ gridTemplateColumns: `repeat(${secondOptions.length}, minmax(0, 1fr))` }}>
|
<div className="grid gap-1.5" style={{ gridTemplateColumns: `repeat(${secondOptions.length}, minmax(0, 1fr)) 72px` }}>
|
||||||
{secondOptions.map((value) => (
|
{secondOptions.map((value) => (
|
||||||
<OptionPill key={value} selected={seconds === String(value)} theme={theme} onClick={() => onConfigChange("videoSeconds", String(value))}>
|
<OptionPill key={value} selected={seconds === String(value)} theme={theme} onClick={() => onConfigChange("videoSeconds", String(value))}>
|
||||||
{value}s
|
{value}s
|
||||||
</OptionPill>
|
</OptionPill>
|
||||||
))}
|
))}
|
||||||
|
<DurationInput value={Number(seconds)} min={VIDEO_DURATION_MIN} theme={theme} onChange={(value) => onConfigChange("videoSeconds", String(value))} />
|
||||||
</div>
|
</div>
|
||||||
</SettingGroup>
|
</SettingGroup>
|
||||||
</div>
|
</div>
|
||||||
@@ -148,13 +149,13 @@ function SeedanceVideoSettingsPanel({ config, onConfigChange, theme, showTitle,
|
|||||||
<button
|
<button
|
||||||
key={item.value}
|
key={item.value}
|
||||||
type="button"
|
type="button"
|
||||||
className="flex h-11 min-w-0 cursor-pointer flex-col items-center justify-center gap-0.5 rounded-md border px-1 text-[10px] font-medium leading-none transition hover:opacity-80"
|
className="flex h-11 min-w-0 cursor-pointer flex-col items-center justify-center gap-0.5 rounded-md px-1 text-[10px] font-medium leading-none transition-colors hover:brightness-110 focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-1"
|
||||||
style={{ background: ratio === item.value ? theme.accent.primarySoft : "transparent", borderColor: ratio === item.value ? theme.accent.primary : theme.node.stroke, color: ratio === item.value ? theme.accent.primary : theme.node.text }}
|
style={{ background: ratio === item.value ? theme.toolbar.activeBg : "transparent", color: theme.node.text, outlineColor: theme.node.muted }}
|
||||||
onMouseDown={(event) => event.stopPropagation()}
|
onMouseDown={(event) => event.stopPropagation()}
|
||||||
onClick={() => onConfigChange("size", item.value)}
|
onClick={() => onConfigChange("size", item.value)}
|
||||||
>
|
>
|
||||||
<span className="grid h-4 place-items-center">
|
<span className="grid h-4 place-items-center">
|
||||||
<SizePreview width={ratioPreview(item.value).width} height={ratioPreview(item.value).height} color={ratio === item.value ? theme.accent.primary : theme.node.text} />
|
<SizePreview width={ratioPreview(item.value).width} height={ratioPreview(item.value).height} color={theme.node.text} />
|
||||||
</span>
|
</span>
|
||||||
<span className="whitespace-nowrap">{item.label}</span>
|
<span className="whitespace-nowrap">{item.label}</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -162,16 +163,17 @@ function SeedanceVideoSettingsPanel({ config, onConfigChange, theme, showTitle,
|
|||||||
</div>
|
</div>
|
||||||
</SettingGroup>
|
</SettingGroup>
|
||||||
<SettingGroup title="时长" color={theme.node.muted}>
|
<SettingGroup title="时长" color={theme.node.muted}>
|
||||||
<div className="grid grid-cols-4 gap-1.5">
|
<div className="grid grid-cols-[repeat(4,minmax(0,1fr))_72px] gap-1.5">
|
||||||
{seedanceDurationOptions.map((value) => (
|
{seedanceDurationOptions.map((value) => (
|
||||||
<OptionPill key={value} selected={duration === value} theme={theme} onClick={() => onConfigChange("videoSeconds", String(value))}>
|
<OptionPill key={value} selected={duration === value} theme={theme} onClick={() => onConfigChange("videoSeconds", String(value))}>
|
||||||
{value}s
|
{value}s
|
||||||
</OptionPill>
|
</OptionPill>
|
||||||
))}
|
))}
|
||||||
|
<DurationInput value={duration} min={VIDEO_DURATION_MIN} theme={theme} onChange={(value) => onConfigChange("videoSeconds", String(value))} />
|
||||||
</div>
|
</div>
|
||||||
</SettingGroup>
|
</SettingGroup>
|
||||||
<SettingGroup title="输出" color={theme.node.muted}>
|
<SettingGroup title="输出" color={theme.node.muted}>
|
||||||
<div className="grid grid-cols-2 gap-3 rounded-md border px-2" style={{ borderColor: theme.node.stroke }}>
|
<div className="grid grid-cols-2 gap-3 rounded-md px-2" style={{ background: theme.toolbar.itemHover }}>
|
||||||
<SwitchRow label="生成声音" checked={generateAudio} theme={theme} onChange={(checked) => onConfigChange("videoGenerateAudio", String(checked))} />
|
<SwitchRow label="生成声音" checked={generateAudio} theme={theme} onChange={(checked) => onConfigChange("videoGenerateAudio", String(checked))} />
|
||||||
<SwitchRow label="添加水印" checked={watermark} theme={theme} onChange={(checked) => onConfigChange("videoWatermark", String(checked))} />
|
<SwitchRow label="添加水印" checked={watermark} theme={theme} onChange={(checked) => onConfigChange("videoWatermark", String(checked))} />
|
||||||
</div>
|
</div>
|
||||||
@@ -209,7 +211,7 @@ export function normalizeVideoResolutionValue(value: string) {
|
|||||||
|
|
||||||
function OptionPill({ selected, disabled = false, theme, onClick, children }: { selected: boolean; disabled?: boolean; theme: CanvasTheme; onClick: () => void; children: ReactNode }) {
|
function OptionPill({ selected, disabled = false, theme, onClick, children }: { selected: boolean; disabled?: boolean; theme: CanvasTheme; onClick: () => void; children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<button type="button" disabled={disabled} className="h-8 cursor-pointer whitespace-nowrap rounded-md border px-1 text-[11px] font-medium leading-none transition hover:opacity-80 disabled:cursor-not-allowed disabled:opacity-35" style={{ background: selected ? theme.accent.primarySoft : "transparent", borderColor: selected ? theme.accent.primary : theme.node.stroke, color: selected ? theme.accent.primary : theme.node.text }} onMouseDown={(event) => event.stopPropagation()} onClick={onClick}>
|
<button type="button" disabled={disabled} className="h-8 cursor-pointer whitespace-nowrap rounded-md px-1 text-[11px] font-medium leading-none transition-colors hover:brightness-110 focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-1 disabled:cursor-not-allowed disabled:opacity-35" style={{ background: selected ? theme.toolbar.activeBg : "transparent", color: theme.node.text, outlineColor: theme.node.muted }} onMouseDown={(event) => event.stopPropagation()} onClick={onClick}>
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
@@ -228,7 +230,7 @@ function SettingGroup({ title, color, children }: { title: string; color: string
|
|||||||
|
|
||||||
function DimensionInput({ prefix, value, disabled, theme, onChange }: { prefix: string; value: number; disabled: boolean; theme: CanvasTheme; onChange: (value: number | null) => void }) {
|
function DimensionInput({ prefix, value, disabled, theme, onChange }: { prefix: string; value: number; disabled: boolean; theme: CanvasTheme; onChange: (value: number | null) => void }) {
|
||||||
return (
|
return (
|
||||||
<label className="flex h-8 overflow-hidden rounded-md border text-[11px]" style={{ background: theme.node.fill, borderColor: theme.node.stroke, color: theme.node.text, opacity: disabled ? 0.55 : 1 }}>
|
<label className="flex h-8 overflow-hidden rounded-md text-[11px]" style={{ background: theme.toolbar.itemHover, color: theme.node.text, opacity: disabled ? 0.55 : 1 }}>
|
||||||
<span className="grid w-7 place-items-center" style={{ color: theme.node.muted }}>
|
<span className="grid w-7 place-items-center" style={{ color: theme.node.muted }}>
|
||||||
{prefix}
|
{prefix}
|
||||||
</span>
|
</span>
|
||||||
@@ -237,6 +239,34 @@ function DimensionInput({ prefix, value, disabled, theme, onChange }: { prefix:
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function DurationInput({ value, min, theme, onChange }: { value: number; min: number; theme: CanvasTheme; onChange: (value: number) => void }) {
|
||||||
|
const commit = (input: HTMLInputElement) => {
|
||||||
|
const next = Math.max(min, Math.floor(Number(input.value) || value || min));
|
||||||
|
input.value = String(next);
|
||||||
|
onChange(next);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<label className="flex h-8 min-w-0 items-center overflow-hidden rounded-md border text-[11px]" style={{ background: theme.toolbar.itemHover, borderColor: theme.toolbar.border, color: theme.node.text }}>
|
||||||
|
<input
|
||||||
|
key={`${min}-${value}`}
|
||||||
|
type="number"
|
||||||
|
inputMode="numeric"
|
||||||
|
min={min}
|
||||||
|
defaultValue={value}
|
||||||
|
aria-label="视频时长(秒)"
|
||||||
|
className="min-w-0 flex-1 bg-transparent pl-2 text-right outline-none [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
|
||||||
|
onBlur={(event) => commit(event.currentTarget)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === "Enter") event.currentTarget.blur();
|
||||||
|
}}
|
||||||
|
onMouseDown={(event) => event.stopPropagation()}
|
||||||
|
/>
|
||||||
|
<span className="shrink-0 px-1.5" style={{ color: theme.node.muted }}>秒</span>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function SizePreview({ width, height, color }: { width: number; height: number; color: string }) {
|
function SizePreview({ width, height, color }: { width: number; height: number; color: string }) {
|
||||||
if (!width || !height) return null;
|
if (!width || !height) return null;
|
||||||
const longSide = Math.max(width, height);
|
const longSide = Math.max(width, height);
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
export const VIDEO_DURATION_OPTIONS = [6, 9, 10, 15] as const;
|
export const VIDEO_DURATION_OPTIONS = [6, 9, 10, 15] as const;
|
||||||
export const VIDEO_RESOLUTION_OPTIONS = [480, 720, 1080] as const;
|
export const VIDEO_RESOLUTION_OPTIONS = [480, 720, 1080] as const;
|
||||||
const NORMALIZED_VIDEO_DURATION_OPTIONS = [5, ...VIDEO_DURATION_OPTIONS] as const;
|
export const VIDEO_DURATION_MIN = 1;
|
||||||
|
|
||||||
export function normalizeVideoDuration(value: string | number | undefined) {
|
export function normalizeVideoDuration(value: string | number | undefined) {
|
||||||
const seconds = Math.floor(Number(value) || VIDEO_DURATION_OPTIONS[0]);
|
const seconds = Math.floor(Number(value) || VIDEO_DURATION_OPTIONS[0]);
|
||||||
return String(nearestOption(seconds, NORMALIZED_VIDEO_DURATION_OPTIONS));
|
return String(Math.max(VIDEO_DURATION_MIN, seconds));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeVideoResolution(value: string | number | undefined) {
|
export function normalizeVideoResolution(value: string | number | undefined) {
|
||||||
|
|||||||
@@ -429,7 +429,7 @@ async function buildSeedanceVideosPayload(config: AiConfig, model: string, promp
|
|||||||
model: modelOptionName(model),
|
model: modelOptionName(model),
|
||||||
prompt: buildSeedanceVideosPromptText(prompt, imageUrls.length, videoUrls.length, audioUrls.length),
|
prompt: buildSeedanceVideosPromptText(prompt, imageUrls.length, videoUrls.length, audioUrls.length),
|
||||||
aspect_ratio: ratio === "adaptive" ? "16:9" : ratio,
|
aspect_ratio: ratio === "adaptive" ? "16:9" : ratio,
|
||||||
duration: duration < 4 ? 5 : duration,
|
duration,
|
||||||
generate_audio: boolConfig(config.videoGenerateAudio, true),
|
generate_audio: boolConfig(config.videoGenerateAudio, true),
|
||||||
...(imageUrls[0] ? { image_url: imageUrls[0] } : {}),
|
...(imageUrls[0] ? { image_url: imageUrls[0] } : {}),
|
||||||
...(imageUrls.length > 1 ? { reference_image_urls: imageUrls.slice(1) } : {}),
|
...(imageUrls.length > 1 ? { reference_image_urls: imageUrls.slice(1) } : {}),
|
||||||
@@ -526,7 +526,7 @@ function assertVideoConfig(config: ResolvedAiConfig, model: string) {
|
|||||||
|
|
||||||
function normalizeVideoSeconds(value: string) {
|
function normalizeVideoSeconds(value: string) {
|
||||||
const seconds = Math.floor(Number(value) || 6);
|
const seconds = Math.floor(Number(value) || 6);
|
||||||
return String(Math.max(1, Math.min(20, seconds)));
|
return String(Math.max(1, seconds));
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeVideoSize(value: string) {
|
function normalizeVideoSize(value: string) {
|
||||||
|
|||||||
Reference in new issue
Block a user