import { modelCapabilityConfigFor, videoDurationAllowed } from "@/lib/model-capabilities"; import { modelOptionName, resolveModelChannel, selectableModelsByCapability, type AiConfig, type ModelCapability } from "@/stores/use-config-store"; export type ModelInputSummary = { textCount: number; imageCount: number; videoCount: number; audioCount: number; characterCount: number; }; export type ModelRequirements = { capability?: ModelCapability; input?: ModelInputSummary; videoOperation?: string; videoSeconds?: string; }; export type DisplayModelGroup = { key: string; label: string; models: string[]; }; export type ModelReferenceLimits = { maxImages: number; maxVideos: number; maxAudios: number; }; export function groupModelsByDisplayName(config: AiConfig, models: string[]): DisplayModelGroup[] { const groups = new Map(); models.forEach((model) => { const channel = resolveModelChannel(config, model); const label = configuredModelDisplayName(config, model); const key = `${channel.id}\u0000${label.toLocaleLowerCase()}`; const current = groups.get(key); if (current) current.models.push(model); else groups.set(key, { key, label, models: [model] }); }); return Array.from(groups.values()); } export function configuredModelDisplayName(config: AiConfig, value: string) { const model = modelOptionName(value); const channel = resolveModelChannel(config, value); return channel.modelCosts?.find((item) => item.model === model)?.displayName?.trim() || model; } export function modelCompatibilityError(config: AiConfig, model: string, requirements?: ModelRequirements) { const capability = requirements?.capability; const input = requirements?.input; if (!capability || !input) return ""; const visualInputCount = input.imageCount + input.characterCount; if (capability === "image") { if (input.videoCount > 0) return "图片模型不支持参考视频"; if (input.audioCount > 0) return "图片模型不支持参考音频"; const maxImages = modelCapabilityConfigFor(config, model).image!.references.maxImages; if (visualInputCount > maxImages) return `最多支持 ${maxImages} 张参考图`; return ""; } if (capability === "video") { const profile = modelCapabilityConfigFor(config, model).video!; if (visualInputCount > profile.references.maxImages) return `最多支持 ${profile.references.maxImages} 张参考图`; if (input.videoCount > profile.references.maxVideos) return `最多支持 ${profile.references.maxVideos} 个参考视频`; if (input.audioCount > profile.references.maxAudios) return `最多支持 ${profile.references.maxAudios} 个参考音频`; if (requirements.videoSeconds && !videoDurationAllowed(profile, Number(requirements.videoSeconds))) return "不支持当前视频时长"; const operation = resolveVideoOperation(input, requirements.videoOperation); if (operation !== "concat" && !profile.operations.includes(operation)) return `不支持${videoOperationLabel(operation)}`; return ""; } if (capability === "text") { return input.audioCount > 0 ? "文本模型不支持参考音频" : ""; } if (input.characterCount > 1) return "角色配音一次只能引用一个角色卡"; return input.imageCount > 0 || input.videoCount > 0 || input.audioCount > 0 ? "音频模型只接受文本或单个角色卡输入" : ""; } export function compatibleModelInGroup(config: AiConfig, models: string[], requirements?: ModelRequirements, preferred?: string) { const ordered = preferred && models.includes(preferred) ? [preferred, ...models.filter((model) => model !== preferred)] : models; return ordered.find((model) => !modelCompatibilityError(config, model, requirements)) || ""; } export function resolveCompatibleModel(config: AiConfig, selected: string, requirements?: ModelRequirements) { if (!requirements?.capability) return selected; const options = selectableModelsByCapability(config, requirements.capability); if (!options.length) return selected; const selectedGroup = groupModelsByDisplayName(config, options).find((group) => group.models.includes(selected)); if (!selectedGroup) return selected; return compatibleModelInGroup(config, selectedGroup.models, requirements, selected); } export function maxModelInputCapacity(config: AiConfig, capability: "image" | "video", kind: "image" | "video" | "audio") { const options = selectableModelsByCapability(config, capability); if (!options.length) return null; return options.reduce((maximum, model) => { const profile = modelCapabilityConfigFor(config, model); const value = capability === "image" ? (kind === "image" ? profile.image!.references.maxImages : 0) : kind === "image" ? profile.video!.references.maxImages : kind === "video" ? profile.video!.references.maxVideos : profile.video!.references.maxAudios; return Math.max(maximum, value); }, 0); } export function modelGroupReferenceLimits(config: AiConfig, selected: string, capability: ModelCapability, requirements?: ModelRequirements): ModelReferenceLimits | undefined { if (capability !== "image" && capability !== "video") return undefined; const options = selectableModelsByCapability(config, capability); const selectedGroup = groupModelsByDisplayName(config, options).find((group) => group.models.includes(selected)); const groupModels = selectedGroup?.models || (selected ? [selected] : []); if (!groupModels.length) return undefined; const compatibleModels = requirements ? groupModels.filter((model) => !modelCompatibilityError(config, model, requirements)) : groupModels; const models = compatibleModels.length ? compatibleModels : groupModels; return models.reduce( (limits, model) => { const profile = modelCapabilityConfigFor(config, model); if (capability === "image") { return { ...limits, maxImages: Math.max(limits.maxImages, profile.image!.references.maxImages) }; } const references = profile.video!.references; return { maxImages: Math.max(limits.maxImages, references.maxImages), maxVideos: Math.max(limits.maxVideos, references.maxVideos), maxAudios: Math.max(limits.maxAudios, references.maxAudios), }; }, { maxImages: 0, maxVideos: 0, maxAudios: 0 }, ); } export function inferVideoOperation(input: ModelInputSummary) { const visualInputCount = input.imageCount + input.characterCount; if (input.audioCount > 0 && visualInputCount === 0 && input.videoCount === 0) return "audio_to_video"; if (input.videoCount > 0) return "extend"; if (visualInputCount > 0) return "image_to_video"; return "text_to_video"; } export function resolveVideoOperation(input: ModelInputSummary, storedOperation?: string) { if (storedOperation && !["text_to_video", "image_to_video", "audio_to_video", "extend"].includes(storedOperation)) return storedOperation; return inferVideoOperation(input); } function videoOperationLabel(operation: string) { if (operation === "text_to_video") return "文生视频"; if (operation === "image_to_video") return "图生视频"; if (operation === "audio_to_video") return "音频生视频"; if (operation === "extend") return "视频续写"; return "当前生成模式"; }