Files
ttstd-infinite-canvas/web/src/lib/model-capabilities.ts

256 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { ModelProtocol } from "@/lib/model-protocols";
export type ModelCapabilityConfig = {
version: number;
image?: ImageCapabilityConfig;
video?: VideoCapabilityConfig;
};
export type ImageSizeParameter = "none" | "size" | "aspect_ratio";
export type ImageCapabilityConfig = {
references: {
promptMaxChars: number;
maxImages: number;
maxImageBytes: number;
maskSupported: boolean;
};
size: {
parameter: ImageSizeParameter;
values: string[];
default: string;
allowCustom: boolean;
};
quality: {
supported: boolean;
values: string[];
default: string;
};
transparentBackground: { supported: boolean; default: boolean };
responseFormat: { supported: boolean };
outputFormat: { supported: boolean };
maxOutputs: number;
};
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;
};
// Keep explicit pixel presets for each resolution tier so the settings panel can
// switch between 1K, 2K and 4K without silently converting the requested ratio.
const defaultImageSizes = [
"auto", "1:1", "3:2", "2:3", "4:3", "3:4", "16:9", "21:9", "9:16",
"1024x1024", "1360x1024", "1024x1360", "1536x1024", "1024x1536", "1024x1280", "1280x1024", "2048x878", "1824x1024", "1024x1824",
"2048x2048", "2304x1728", "1728x2304", "2496x1664", "1664x2496", "1792x2240", "2240x1792", "3136x1344", "2752x1536", "1536x2752",
"2880x2880", "3264x2448", "2448x3264", "3504x2336", "2336x3504", "2560x3200", "3200x2560", "3808x1632", "3840x2160", "2160x3840",
];
export function defaultImageCapabilityConfig(protocol?: ModelProtocol, model = ""): ImageCapabilityConfig {
const image: ImageCapabilityConfig = {
references: { promptMaxChars: 32000, maxImages: 16, maxImageBytes: 30 * 1024 * 1024, maskSupported: true },
size: { parameter: "size", values: [...defaultImageSizes], default: "1:1", allowCustom: true },
quality: { supported: true, values: ["auto", "low", "medium", "high"], default: "auto" },
transparentBackground: { supported: true, default: false },
responseFormat: { supported: true },
outputFormat: { supported: true },
maxOutputs: 15,
};
if (protocol === "grok-image") {
image.references.maxImages = 1;
image.references.maskSupported = false;
// grok2api / xAI Imagine:size→aspect_ratio,quality→resolution(1k/2k)。
image.size = {
parameter: "aspect_ratio",
values: ["1:1", "3:4", "4:3", "9:16", "16:9", "2:3", "3:2"],
default: "1:1",
allowCustom: false,
};
image.quality = { supported: true, values: ["1k", "2k"], default: "2k" };
image.transparentBackground = { supported: false, default: false };
image.responseFormat = { supported: true };
image.outputFormat = { supported: false };
image.maxOutputs = 1;
} else if (protocol === "volcengine-ark-image") {
image.references.maskSupported = false;
image.quality.supported = false;
image.transparentBackground.supported = false;
image.responseFormat.supported = false;
image.outputFormat.supported = false;
}
if (protocol === "volcengine-jimeng-image") {
image.references.maxImages = 14;
image.references.maskSupported = false;
image.quality.supported = false;
image.transparentBackground.supported = false;
image.responseFormat.supported = false;
image.outputFormat.supported = false;
}
if (protocol !== "grok-image" && model.trim().toLowerCase().startsWith("grok-imagine-image")) {
image.references.maxImages = 0;
image.references.maskSupported = false;
image.size = {
parameter: "aspect_ratio",
values: ["1:1", "3:4", "4:3", "9:16", "16:9", "2:3", "3:2"],
default: "1:1",
allowCustom: false,
};
image.quality = { supported: true, values: ["1k", "2k"], default: "2k" };
image.transparentBackground = { supported: false, default: false };
image.responseFormat = { supported: true };
image.outputFormat = { supported: false };
image.maxOutputs = 1;
}
return image;
}
export function defaultModelCapabilityConfig(protocol?: ModelProtocol, model = ""): 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", "1440p", "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 };
video.resolutions = ["720p"];
}
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" || protocol === "newapi-channel-1") video.resolutions = ["480p", "720p", "1080p"];
if (protocol === "volcengine-ark-video") video.watermark = { supported: true, default: false };
if (protocol === "novita-video") {
video.references.maxImages = 1;
video.references.maxImageBytes = 10 * 1024 * 1024;
video.duration = { selection: "enum", values: [5, 10], default: 5 };
video.ratios = ["16:9", "9:16", "1:1"];
video.resolutions = ["1080p"];
video.defaultResolution = "1080p";
}
return { version: 1, image: defaultImageCapabilityConfig(protocol, model), 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);
const fallback = defaultModelCapabilityConfig(cost?.protocol, modelName);
return cost?.capabilityConfig
? { ...fallback, ...cost.capabilityConfig, image: cost.capabilityConfig.image || fallback.image, video: cost.capabilityConfig.video || fallback.video }
: fallback;
}
export function normalizeImageValue(profile: ImageCapabilityConfig, value: { size?: string; quality?: string; count?: string; transparentBackground?: string }) {
const size = normalizeImageSizeSetting(profile, value.size);
const quality = profile.quality.supported
? (value.quality && profile.quality.values.includes(value.quality) ? value.quality : profile.quality.default || "auto")
: profile.quality.default || "auto";
const count = String(Math.max(1, Math.min(profile.maxOutputs, Math.floor(Math.abs(Number(value.count)) || 1))));
const transparentBackground = profile.transparentBackground.supported && value.transparentBackground === "true" ? "true" : "false";
return { size, quality, count, transparentBackground };
}
export function normalizeImageSizeSetting(profile: ImageCapabilityConfig, value?: string) {
if (profile.size.parameter === "none") return "auto";
const candidate = value?.trim() || profile.size.default;
if (profile.size.allowCustom || profile.size.values.includes(candidate)) return candidate;
return profile.size.default || profile.size.values[0] || "auto";
}
export function imageSizeRequest(profile: ImageCapabilityConfig, value?: string) {
const parameter = profile.size.parameter;
if (parameter === "none") return undefined;
const normalized = normalizeImageSizeSetting(profile, value);
if (!normalized || normalized === "auto") return undefined;
return { parameter, value: normalized };
}
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;
}