feat(*): 模型能力配置 - 支持模型级视频限制校验与协议管理
This commit is contained in:
@@ -33,16 +33,17 @@ export function getAntThemeConfig(dark: boolean): ThemeConfig {
|
||||
const elevatedBackground = dark ? "rgba(31, 31, 32, 0.96)" : "rgba(255, 255, 255, 0.96)";
|
||||
const subtleBackground = dark ? "rgba(255, 255, 255, 0.055)" : "rgba(17, 17, 17, 0.035)";
|
||||
const interactiveBorder = dark ? "rgba(255, 255, 255, 0.18)" : "rgba(17, 17, 17, 0.18)";
|
||||
const focusShadow = dark ? "0 0 0 3px rgba(255, 255, 255, 0.12)" : "0 0 0 3px rgba(17, 17, 17, 0.08)";
|
||||
// 黑白主题使用边框表达焦点,避免输入控件周围出现蓝紫色光圈。
|
||||
const focusShadow = "none";
|
||||
// 暗色界面的黑白主按钮与选择控件分色,避免开关轨道、勾选符号和滑块融成一片。
|
||||
const selectionControl = dark
|
||||
? {
|
||||
active: "#4657c5",
|
||||
active: "#ffffff",
|
||||
border: "rgba(255, 255, 255, 0.3)",
|
||||
disabledBackground: "rgba(255, 255, 255, 0.06)",
|
||||
focus: "rgba(116, 133, 238, 0.28)",
|
||||
hover: "#7485ee",
|
||||
primary: "#5b6ee1",
|
||||
focus: "transparent",
|
||||
hover: "#e5e5e5",
|
||||
primary: "#f5f5f5",
|
||||
surface: "rgba(255, 255, 255, 0.035)",
|
||||
}
|
||||
: null;
|
||||
@@ -180,7 +181,7 @@ export function getAntThemeConfig(dark: boolean): ThemeConfig {
|
||||
multipleItemHeight: 24,
|
||||
activeBorderColor: interactiveBorder,
|
||||
hoverBorderColor: interactiveBorder,
|
||||
activeOutlineColor: dark ? "rgba(255, 255, 255, 0.12)" : "rgba(17, 17, 17, 0.08)",
|
||||
activeOutlineColor: "transparent",
|
||||
optionActiveBg: color.selectActiveBg,
|
||||
optionSelectedBg: color.selectSelectedBg,
|
||||
optionSelectedColor: color.selectText,
|
||||
|
||||
121
web/src/lib/model-capabilities.ts
Normal file
121
web/src/lib/model-capabilities.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import type { ModelProtocol } from "@/lib/model-protocols";
|
||||
|
||||
export type ModelCapabilityConfig = {
|
||||
version: number;
|
||||
video?: VideoCapabilityConfig;
|
||||
};
|
||||
|
||||
export type VideoCapabilityConfig = {
|
||||
references: {
|
||||
promptMaxChars: number;
|
||||
maxImages: number;
|
||||
maxImageBytes: number;
|
||||
maxVideos: number;
|
||||
maxVideoBytes: number;
|
||||
maxVideoDurationSeconds: number;
|
||||
maxAudios: number;
|
||||
maxAudioBytes: number;
|
||||
maxAudioDurationSeconds: number;
|
||||
};
|
||||
duration: {
|
||||
selection: "range" | "enum";
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
values?: number[];
|
||||
default: number;
|
||||
};
|
||||
ratios: string[];
|
||||
defaultRatio: string;
|
||||
resolutions: string[];
|
||||
defaultResolution: string;
|
||||
generateAudio: { supported: boolean; default: boolean };
|
||||
watermark: { supported: boolean; default: boolean };
|
||||
operations: string[];
|
||||
defaultOperation: string;
|
||||
};
|
||||
|
||||
export function defaultModelCapabilityConfig(protocol?: ModelProtocol): ModelCapabilityConfig {
|
||||
const video: VideoCapabilityConfig = {
|
||||
references: {
|
||||
promptMaxChars: 1000,
|
||||
maxImages: 9,
|
||||
maxImageBytes: 30 * 1024 * 1024,
|
||||
maxVideos: 0,
|
||||
maxVideoBytes: 0,
|
||||
maxVideoDurationSeconds: 0,
|
||||
maxAudios: 0,
|
||||
maxAudioBytes: 0,
|
||||
maxAudioDurationSeconds: 0,
|
||||
},
|
||||
duration: { selection: "range", min: 1, max: 15, step: 1, default: 6 },
|
||||
ratios: ["16:9", "9:16", "1:1", "4:3", "3:4", "21:9"],
|
||||
defaultRatio: "16:9",
|
||||
resolutions: ["480p", "720p", "1080p", "2160p"],
|
||||
defaultResolution: "720p",
|
||||
generateAudio: { supported: false, default: false },
|
||||
watermark: { supported: false, default: false },
|
||||
operations: ["text_to_video", "image_to_video"],
|
||||
defaultOperation: "text_to_video",
|
||||
};
|
||||
if (protocol === "volcengine-jimeng-video") video.duration = { selection: "enum", values: [5, 10], default: 5 };
|
||||
if (protocol === "gemini-veo") {
|
||||
video.duration = { selection: "enum", values: [4, 6, 8], default: 6 };
|
||||
video.resolutions = ["720p", "1080p"];
|
||||
}
|
||||
if (protocol === "volcengine-ark-video" || protocol === "newapi-channel-1" || protocol === "newapi-channel-2") {
|
||||
video.references.maxVideos = 3;
|
||||
video.references.maxAudios = 3;
|
||||
video.references.maxVideoBytes = 200 * 1024 * 1024;
|
||||
video.references.maxAudioBytes = 15 * 1024 * 1024;
|
||||
video.references.maxVideoDurationSeconds = 15;
|
||||
video.references.maxAudioDurationSeconds = 15;
|
||||
video.generateAudio = { supported: true, default: true };
|
||||
}
|
||||
if (protocol === "volcengine-ark-video") video.watermark = { supported: true, default: false };
|
||||
return { version: 1, video };
|
||||
}
|
||||
|
||||
export function modelCapabilityConfigFor(config: { channels: Array<{ id: string; models: string[]; modelCosts?: Array<{ model: string; capabilityConfig?: ModelCapabilityConfig; protocol?: ModelProtocol }> }> }, model: string) {
|
||||
const separator = model.indexOf("::");
|
||||
const channelId = separator >= 0 ? model.slice(0, separator) : "";
|
||||
const modelName = separator >= 0 ? model.slice(separator + 2) : model;
|
||||
const channel = config.channels.find((item) => item.id === channelId) || config.channels.find((item) => item.models.includes(modelName));
|
||||
const cost = channel?.modelCosts?.find((item) => item.model === modelName);
|
||||
return cost?.capabilityConfig || defaultModelCapabilityConfig(cost?.protocol);
|
||||
}
|
||||
|
||||
export function normalizeVideoValue(profile: VideoCapabilityConfig, value: { seconds?: string; ratio?: string; resolution?: string }) {
|
||||
const duration = profile.duration.selection === "enum"
|
||||
? (profile.duration.values || []).includes(Number(value.seconds)) ? Number(value.seconds) : profile.duration.default
|
||||
: normalizeRangeDuration(profile, Number(value.seconds));
|
||||
const ratio = profile.ratios.includes(value.ratio || "") ? value.ratio! : profile.defaultRatio;
|
||||
const resolution = profile.resolutions.includes(value.resolution || "") ? value.resolution! : profile.defaultResolution;
|
||||
return { seconds: String(duration), ratio, resolution };
|
||||
}
|
||||
|
||||
function normalizeRangeDuration(profile: VideoCapabilityConfig, value: number) {
|
||||
const min = profile.duration.min || 1;
|
||||
const max = profile.duration.max || min;
|
||||
const step = profile.duration.step || 1;
|
||||
const candidate = Number.isFinite(value) ? Math.floor(value) : profile.duration.default;
|
||||
const clamped = Math.min(max, Math.max(min, candidate));
|
||||
const maxStep = Math.max(0, Math.floor((max - min) / step));
|
||||
return min + Math.min(maxStep, Math.max(0, Math.round((clamped - min) / step))) * step;
|
||||
}
|
||||
|
||||
export function videoDurationOptions(profile: VideoCapabilityConfig) {
|
||||
if (profile.duration.selection === "enum") return profile.duration.values || [];
|
||||
const min = profile.duration.min || 1;
|
||||
const max = profile.duration.max || min;
|
||||
const step = profile.duration.step || 1;
|
||||
return Array.from({ length: Math.floor((max - min) / step) + 1 }, (_, index) => min + index * step);
|
||||
}
|
||||
|
||||
export function videoDurationAllowed(profile: VideoCapabilityConfig, value: number) {
|
||||
if (profile.duration.selection === "enum") return (profile.duration.values || []).includes(value);
|
||||
const min = profile.duration.min || 1;
|
||||
const max = profile.duration.max || min;
|
||||
const step = profile.duration.step || 1;
|
||||
return value >= min && value <= max && (value - min) % step === 0;
|
||||
}
|
||||
@@ -21,6 +21,7 @@ export async function testChannelModelConnection(channel: ModelChannel, model: s
|
||||
protocol: testProtocol,
|
||||
billingMode: modelCost?.billingMode || "fixed_request",
|
||||
unitPriceMicrocredits: modelCost?.unitPriceMicrocredits || 0,
|
||||
capabilityConfig: modelCost?.capabilityConfig,
|
||||
},
|
||||
...(channel.modelCosts || []).filter((item) => item.model !== model),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user