mirror of
https://github.com/basketikun/infinite-canvas.git
synced 2026-08-03 07:21:13 +08:00
feat: add seedance media params and model visibility
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { type ReactNode } from "react";
|
||||
import { Switch } from "antd";
|
||||
|
||||
import { ImageSettingsTheme } from "@/components/image-settings-panel";
|
||||
import { boolConfig, isSeedanceFastModel, isSeedanceVideoConfig, normalizeSeedanceDuration, normalizeSeedanceRatio, normalizeSeedanceResolution, seedanceDurationOptions, seedancePixelLabel, seedanceRatioOptions, seedanceResolutionOptions } from "@/lib/seedance-video";
|
||||
import { type CanvasTheme } from "@/lib/canvas-theme";
|
||||
import type { AiConfig } from "@/stores/use-config-store";
|
||||
|
||||
@@ -24,13 +26,17 @@ const secondOptions = [6, 10, 12, 16, 20];
|
||||
|
||||
type VideoSettingsPanelProps = {
|
||||
config: AiConfig;
|
||||
onConfigChange: (key: "vquality" | "size" | "videoSeconds", value: string) => void;
|
||||
onConfigChange: (key: "vquality" | "size" | "videoSeconds" | "videoGenerateAudio" | "videoWatermark", value: string) => void;
|
||||
theme: CanvasTheme;
|
||||
showTitle?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function VideoSettingsPanel({ config, onConfigChange, theme, showTitle = true, className = "w-[320px] space-y-4 rounded-2xl px-1 py-0.5" }: VideoSettingsPanelProps) {
|
||||
if (isSeedanceVideoConfig(config)) {
|
||||
return <SeedanceVideoSettingsPanel config={config} onConfigChange={onConfigChange} theme={theme} showTitle={showTitle} className={className} />;
|
||||
}
|
||||
|
||||
const seconds = config.videoSeconds || "6";
|
||||
const size = normalizeVideoSizeValue(config.size);
|
||||
const dimensions = readSizeDimensions(size);
|
||||
@@ -96,16 +102,84 @@ export function VideoSettingsPanel({ config, onConfigChange, theme, showTitle =
|
||||
);
|
||||
}
|
||||
|
||||
function SeedanceVideoSettingsPanel({ config, onConfigChange, theme, showTitle, className }: VideoSettingsPanelProps) {
|
||||
const model = config.model || config.videoModel;
|
||||
const resolution = normalizeSeedanceResolution(config.vquality, model);
|
||||
const ratio = normalizeSeedanceRatio(config.size);
|
||||
const duration = normalizeSeedanceDuration(config.videoSeconds);
|
||||
const generateAudio = boolConfig(config.videoGenerateAudio, true);
|
||||
const watermark = boolConfig(config.videoWatermark, false);
|
||||
|
||||
return (
|
||||
<ImageSettingsTheme theme={theme}>
|
||||
<div className={className} style={{ color: theme.node.text }} onMouseDown={(event) => event.stopPropagation()}>
|
||||
{showTitle ? <div className="text-lg font-semibold">视频设置</div> : null}
|
||||
<SettingGroup title="分辨率" color={theme.node.muted}>
|
||||
<div className="grid grid-cols-3 gap-2.5">
|
||||
{seedanceResolutionOptions.map((item) => {
|
||||
const disabled = item.value === "1080p" && isSeedanceFastModel(model);
|
||||
return (
|
||||
<OptionPill key={item.value} selected={resolution === item.value} disabled={disabled} theme={theme} onClick={() => onConfigChange("vquality", item.value)}>
|
||||
{item.label}
|
||||
</OptionPill>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{isSeedanceFastModel(model) ? <div className="text-[11px] leading-4 opacity-55">fast 模型不支持 1080p,会自动使用 720p。</div> : null}
|
||||
</SettingGroup>
|
||||
<SettingGroup title="比例" color={theme.node.muted}>
|
||||
<div className="grid grid-cols-3 gap-2.5">
|
||||
{seedanceRatioOptions.map((item) => (
|
||||
<button
|
||||
key={item.value}
|
||||
type="button"
|
||||
className="flex h-[68px] cursor-pointer flex-col items-center justify-center gap-1 rounded-xl border bg-transparent px-1 text-sm transition hover:opacity-80"
|
||||
style={{ borderColor: ratio === item.value ? theme.node.text : theme.node.stroke, color: theme.node.text }}
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
onClick={() => onConfigChange("size", item.value)}
|
||||
>
|
||||
<SizePreview width={ratioPreview(item.value).width} height={ratioPreview(item.value).height} color={theme.node.text} />
|
||||
<span>{item.label}</span>
|
||||
<span className="text-[10px] leading-none opacity-55">{item.value === "adaptive" ? "adaptive" : seedancePixelLabel(resolution, item.value)}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</SettingGroup>
|
||||
<SettingGroup title="时长" color={theme.node.muted}>
|
||||
<div className="grid grid-cols-4 gap-2.5">
|
||||
{seedanceDurationOptions.map((value) => (
|
||||
<OptionPill key={value} selected={duration === value} theme={theme} onClick={() => onConfigChange("videoSeconds", String(value))}>
|
||||
{value === -1 ? "智能" : `${value}s`}
|
||||
</OptionPill>
|
||||
))}
|
||||
</div>
|
||||
<NumberInput value={String(duration)} min={-1} max={15} theme={theme} onChange={(value) => onConfigChange("videoSeconds", value)} />
|
||||
</SettingGroup>
|
||||
<SettingGroup title="输出" color={theme.node.muted}>
|
||||
<div className="grid gap-2 rounded-xl border p-2.5" style={{ borderColor: theme.node.stroke }}>
|
||||
<SwitchRow label="生成声音" checked={generateAudio} theme={theme} onChange={(checked) => onConfigChange("videoGenerateAudio", String(checked))} />
|
||||
<SwitchRow label="添加水印" checked={watermark} theme={theme} onChange={(checked) => onConfigChange("videoWatermark", String(checked))} />
|
||||
</div>
|
||||
</SettingGroup>
|
||||
</div>
|
||||
</ImageSettingsTheme>
|
||||
);
|
||||
}
|
||||
|
||||
export function videoResolutionLabel(value: string) {
|
||||
return `${normalizeVideoResolutionValue(value)}p`;
|
||||
}
|
||||
|
||||
export function videoSizeLabel(value: string) {
|
||||
const ratio = normalizeSeedanceRatio(value);
|
||||
if (value === "adaptive" || value === "auto") return "自适应";
|
||||
if (ratio === value) return seedanceRatioOptions.find((item) => item.value === ratio)?.label || ratio;
|
||||
const size = normalizeVideoSizeValue(value);
|
||||
return sizeOptions.find((item) => item.value === size)?.label || size;
|
||||
}
|
||||
|
||||
export function videoSecondsLabel(value: string) {
|
||||
if (String(value).trim() === "-1") return "智能";
|
||||
return `${value || "6"}s`;
|
||||
}
|
||||
|
||||
@@ -121,9 +195,9 @@ export function normalizeVideoResolutionValue(value: string) {
|
||||
return value.replace(/p$/i, "") || "720";
|
||||
}
|
||||
|
||||
function OptionPill({ selected, theme, onClick, children }: { selected: 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 (
|
||||
<button type="button" className="h-9 cursor-pointer rounded-full border px-2 text-sm transition hover:opacity-80" style={{ background: "transparent", borderColor: selected ? theme.node.text : theme.node.stroke, color: theme.node.text }} onMouseDown={(event) => event.stopPropagation()} onClick={onClick}>
|
||||
<button type="button" disabled={disabled} className="h-9 cursor-pointer rounded-full border px-2 text-sm transition hover:opacity-80 disabled:cursor-not-allowed disabled:opacity-35" style={{ background: "transparent", borderColor: selected ? theme.node.text : theme.node.stroke, color: theme.node.text }} onMouseDown={(event) => event.stopPropagation()} onClick={onClick}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
@@ -174,6 +248,29 @@ function SizePreview({ width, height, color }: { width: number; height: number;
|
||||
return <span className="rounded-[3px] border-2" style={{ width: previewWidth, height: previewHeight, borderColor: color }} />;
|
||||
}
|
||||
|
||||
function ratioPreview(ratio: string) {
|
||||
if (ratio === "9:16") return { width: 9, height: 16 };
|
||||
if (ratio === "1:1") return { width: 1, height: 1 };
|
||||
if (ratio === "4:3") return { width: 4, height: 3 };
|
||||
if (ratio === "3:4") return { width: 3, height: 4 };
|
||||
if (ratio === "21:9") return { width: 21, height: 9 };
|
||||
if (ratio === "adaptive") return { width: 0, height: 0 };
|
||||
return { width: 16, height: 9 };
|
||||
}
|
||||
|
||||
function SwitchRow({ label, checked, theme, onChange }: { label: string; checked: boolean; theme: CanvasTheme; onChange: (checked: boolean) => void }) {
|
||||
return (
|
||||
<div className="flex h-8 items-center justify-between gap-3">
|
||||
<span className="text-sm" style={{ color: theme.node.text }}>
|
||||
{label}
|
||||
</span>
|
||||
<span onMouseDown={(event) => event.stopPropagation()}>
|
||||
<Switch size="small" checked={checked} onChange={onChange} />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function readSizeDimensions(size: string) {
|
||||
if (size === "auto") return { width: 0, height: 0 };
|
||||
const match = size.match(/^(\d+)x(\d+)$/);
|
||||
|
||||
Reference in New Issue
Block a user