import { Input, InputNumber, Segmented, Select, Switch } from "antd"; import type { ReactNode } from "react"; import { defaultImageCapabilityConfig, defaultModelCapabilityConfig, normalizeModelCapabilityConfig, type ImageCapabilityConfig, type ModelCapabilityConfig, type TextCapabilityConfig, type VideoCapabilityConfig } from "@/lib/model-capabilities"; import type { ModelProtocol } from "@/lib/model-protocols"; import { VIDEO_RESOLUTION_CAPABILITY_OPTIONS } from "@/lib/video-generation-options"; const ratioOptions = ["16:9", "9:16", "1:1", "4:3", "3:4", "21:9"]; const operationOptions = [ { label: "文生视频", value: "text_to_video" }, { label: "图生视频", value: "image_to_video" }, { label: "全模态参考", value: "reference_to_video" }, { label: "视频续写", value: "extend" }, { label: "局部修改", value: "inpaint" }, { label: "元素替换", value: "replace_element" }, { label: "运镜调整", value: "camera_motion" }, { label: "风格迁移", value: "style_transfer" }, { label: "音频生视频", value: "audio_to_video" }, ]; function formatImageSizeTagLabel(size: string): string { const raw = size.trim(); if (raw === "1:1") return "1:1 (方图)"; if (raw === "16:9") return "16:9 (横屏)"; if (raw === "9:16") return "9:16 (竖屏)"; if (raw === "1024x1024") return "1024x1024 (1:1)"; if (raw === "2048x2048") return "2048x2048 (2K 方)"; if (raw === "3840x2160") return "3840x2160 (4K 横屏)"; if (raw === "2160x3840") return "2160x3840 (4K 竖屏)"; return raw; } function imageSizeOptions(values: string[]) { return values.map((v) => ({ label: formatImageSizeTagLabel(v), value: v })); } type Props = { value?: ModelCapabilityConfig; onChange?: (value: ModelCapabilityConfig) => void; protocol?: ModelProtocol; capability?: "text" | "image" | "video"; model?: string; disabled?: boolean; section?: "all" | "protocol" | "references"; }; export function ModelCapabilityEditor({ value, onChange, protocol, capability = "video", model = "", disabled = false, section = "all" }: Props) { if (capability === "text") { return ; } if (capability === "image") { return ; } const profile = normalizeModelCapabilityConfig(value || defaultModelCapabilityConfig(protocol)).video!; const update = (patch: Partial) => onChange?.({ version: 1, video: { ...profile, ...patch } }); const updateReferences = (patch: Partial) => update({ references: { ...profile.references, ...patch } }); const updateDuration = (patch: Partial) => update({ duration: { ...profile.duration, ...patch } }); const durationValues = (profile.duration.values || []).join(","); const resolutionOptions = Array.from(new Set([...VIDEO_RESOLUTION_CAPABILITY_OPTIONS, ...profile.resolutions])); if (section === "references") { return (
updateReferences({ minImages: value || 0 })} /> { const maxImages = value || 0; updateReferences({ maxImages, minImages: Math.min(profile.references.minImages, maxImages) }); }} /> updateReferences({ maxImageBytes: mbToBytes(value) })} /> updateReferences({ maxAudios: value || 0 })} /> updateReferences({ maxAudioBytes: mbToBytes(value) })} /> updateReferences({ maxAudioDurationSeconds: value || 0 })} /> update({ generateAudio })} /> updateReferences({ maxVideos: value || 0 })} /> updateReferences({ maxVideoBytes: mbToBytes(value) })} /> updateReferences({ maxVideoDurationSeconds: value || 0 })} /> updateReferences({ promptMaxChars: value || 1 })} /> update({ watermark })} />
); } if (section === "protocol") { return (
profile.operations.includes(item.value))} onChange={(defaultOperation) => update({ defaultOperation })} /> updateDuration( value === "enum" ? { selection: "enum", values: profile.duration.values?.length ? profile.duration.values : [profile.duration.default] } : { selection: "range", min: profile.duration.min || 1, max: profile.duration.max || 15, step: profile.duration.step || 1 }, ) } /> {profile.duration.selection === "enum" ? ( updateDuration({ values: parseIntegerList(event.target.value), default: parseIntegerList(event.target.value)[0] || profile.duration.default })} /> ) : (
updateDuration({ min: value || 1 })} /> updateDuration({ max: value || 1 })} /> updateDuration({ step: value || 1 })} /> updateDuration({ default: value || 1 })} />
)}
({ label: item, value: item }))} onChange={(defaultRatio) => update({ defaultRatio })} /> ({ label: item.toUpperCase(), value: item }))} onChange={(defaultResolution) => update({ defaultResolution })} />
); } return (
视频能力参数
这些参数会同步到创造页、画布和生成校验
协议模板可继续调整
profile.operations.includes(item.value))} onChange={(defaultOperation) => update({ defaultOperation })} />
updateDuration( value === "enum" ? { selection: "enum", values: profile.duration.values?.length ? profile.duration.values : [profile.duration.default] } : { selection: "range", min: profile.duration.min || 1, max: profile.duration.max || 15, step: profile.duration.step || 1 }, ) } /> {profile.duration.selection === "enum" ? ( updateDuration({ values: parseIntegerList(event.target.value), default: parseIntegerList(event.target.value)[0] || profile.duration.default })} /> ) : (
updateDuration({ min: value || 1 })} /> updateDuration({ max: value || 1 })} /> updateDuration({ step: value || 1 })} /> updateDuration({ default: value || 1 })} />
)}
({ label: item, value: item }))} onChange={(defaultRatio) => update({ defaultRatio })} /> ({ label: item.toUpperCase(), value: item }))} onChange={(defaultResolution) => update({ defaultResolution })} />
{section === "all" ? (
updateReferences({ maxVideos: value || 0 })} /> updateReferences({ maxVideoBytes: mbToBytes(value) })} /> updateReferences({ maxVideoDurationSeconds: value || 0 })} />
) : null}
{section === "all" ? ( <>
updateReferences({ minImages: value || 0 })} /> { const maxImages = value || 0; updateReferences({ maxImages, minImages: Math.min(profile.references.minImages, maxImages) }); }} /> updateReferences({ maxImageBytes: mbToBytes(value) })} />
updateReferences({ maxAudios: value || 0 })} /> updateReferences({ maxAudioBytes: mbToBytes(value) })} /> updateReferences({ maxAudioDurationSeconds: value || 0 })} />
update({ generateAudio })} />
updateReferences({ promptMaxChars: value || 1 })} /> update({ watermark })} />
) : null}
); } function TextCapabilityEditor({ value, onChange, protocol, disabled, section }: Pick) { const profile = value?.text || defaultModelCapabilityConfig(protocol).text!; const updateReferences = (patch: Partial) => { onChange?.({ version: 1, text: { references: { ...profile.references, ...patch } } }); }; if (section === "references") { return (
updateReferences({ maxImages: next || 0 })} /> updateReferences({ maxImageBytes: mbToBytes(next) })} /> updateReferences({ maxVideos: next || 0 })} /> updateReferences({ maxVideoBytes: mbToBytes(next) })} /> updateReferences({ promptMaxChars: next || 1 })} />
); } return (
文本理解能力
默认不假设支持图片或视频,只有明确配置后相关请求才会进入该模型。
updateReferences({ maxImages: next || 0 })} /> updateReferences({ maxImageBytes: mbToBytes(next) })} />
updateReferences({ maxVideos: next || 0 })} /> updateReferences({ maxVideoBytes: mbToBytes(next) })} />
updateReferences({ promptMaxChars: next || 1 })} />
); } function ImageCapabilityEditor({ value, onChange, protocol, model, disabled, section }: Required> & Pick) { const profile = normalizeModelCapabilityConfig(value || { version: 1, image: defaultImageCapabilityConfig(protocol, model) }).image!; const update = (patch: Partial) => onChange?.({ version: 1, image: { ...profile, ...patch } }); const updateReferences = (patch: Partial) => update({ references: { ...profile.references, ...patch } }); const updateSize = (patch: Partial) => update({ size: { ...profile.size, ...patch } }); const updateQuality = (patch: Partial) => update({ quality: { ...profile.quality, ...patch } }); if (section === "references") { return (
updateReferences({ maxImages: maxImages || 0 })} /> updateReferences({ maxImageBytes: mbToBytes(maxImageBytes) })} /> updateReferences({ maskSupported })} /> updateReferences({ promptMaxChars: promptMaxChars || 1 })} />
); } if (section === "protocol") { return (
update({ maxOutputs: maxOutputs || 1 })} /> { const parameter = value as ImageCapabilityConfig["size"]["parameter"]; updateSize( parameter === "none" ? { parameter, values: [], default: "auto", allowCustom: false } : { parameter, values: profile.size.values.length ? profile.size.values : ["1:1"], default: profile.size.default === "auto" ? "1:1" : profile.size.default }, ); }} /> {profile.size.parameter !== "none" ? ( <> ({ label: item, value: item }))} onChange={(defaultValue) => updateSize({ default: defaultValue })} /> updateSize({ allowCustom })} /> ) : null} updateQuality({ supported })} /> {profile.quality.supported ? (
({ label: item, value: item }))} onChange={(defaultValue) => updateQuality({ default: defaultValue })} />
) : null} update({ transparentBackground })} /> update({ responseFormat: { supported } })} /> update({ outputFormat: { supported } })} />
); } return (
图片能力参数
生成界面和后端请求都会按此处裁剪参数
当前模型独立生效
updateReferences({ maxImages: maxImages || 0 })} /> updateReferences({ maxImageBytes: mbToBytes(maxImageBytes) })} />
updateReferences({ maskSupported })} />
update({ maxOutputs: maxOutputs || 1 })} /> { const parameter = value as ImageCapabilityConfig["size"]["parameter"]; updateSize( parameter === "none" ? { parameter, values: [], default: "auto", allowCustom: false } : { parameter, values: profile.size.values.length ? profile.size.values : ["1:1"], default: profile.size.default === "auto" ? "1:1" : profile.size.default }, ); }} /> {profile.size.parameter !== "none" ? ( <> ({ label: item, value: item }))} onChange={(defaultValue) => updateSize({ default: defaultValue })} /> updateSize({ allowCustom })} />
) : null} updateQuality({ supported })} /> {profile.quality.supported ? (
({ label: item, value: item }))} onChange={(defaultValue) => updateQuality({ default: defaultValue })} />
) : null} update({ transparentBackground })} />
update({ responseFormat: { supported } })} /> update({ outputFormat: { supported } })} />
updateReferences({ promptMaxChars: promptMaxChars || 1 })} /> ); } function CapabilityGroup({ title, description, children }: { title: string; description?: string; children: ReactNode }) { return (
{title} {description ? {description} : null}
{children}
); } function CapabilityBlock({ title, children }: { title: string; children: ReactNode }) { return (
{title}
{children}
); } function ReferenceCard({ title, description, children }: { title: string; description: string; children: ReactNode }) { return (

{title}

{description}

{children}
); } function ProtocolParameterCard({ step, title, description, children }: { step: string; title: string; description: string; children: ReactNode }) { return (
{step}

{title}

{description}

{children}
); } function Field({ label, children }: { label: string; children: ReactNode }) { return ( ); } function NumberField({ label, value, min, max, disabled, onChange }: { label: string; value?: number; min: number; max?: number; disabled: boolean; onChange: (value: number | null) => void }) { return ( ); } function BooleanField({ label, value, disabled, onChange }: { label: string; value: { supported: boolean; default: boolean }; disabled: boolean; onChange: (value: { supported: boolean; default: boolean }) => void }) { return (
{label}
可发送参数与默认值
); } function ParameterField({ label, description, supported, disabled, onChange }: { label: string; description: string; supported: boolean; disabled: boolean; onChange: (supported: boolean) => void }) { return (
{label}
{description}
); } function bytesToMB(value: number) { return value ? Math.round((value / (1024 * 1024)) * 10) / 10 : 0; } function mbToBytes(value: number | null) { return Math.max(0, Math.round(Number(value || 0) * 1024 * 1024)); } function parseIntegerList(value: string) { return Array.from( new Set( value .split(",") .map((item) => Number(item.trim())) .filter((item) => Number.isInteger(item) && item > 0), ), ).sort((left, right) => left - right); }