feat(studio): refine model picker catalog

This commit is contained in:
antilya
2026-08-20 21:51:31 +04:00
parent 9716fe9336
commit 841c6e199d
3 changed files with 783 additions and 108 deletions
+56 -39
View File
@@ -23,6 +23,11 @@ import {
getDefaultEffectForI2IModel,
getI2IModelById,
} from "../models.js";
import {
imageModelCatalog,
imageModelPickerEntries,
imageModelPickerEntryByVariantId,
} from "../modelFamilies.js";
import {
PROMPT_CONTROL_LABEL_CLASS,
PROMPT_MEDIA_PREVIEW_CLASS,
@@ -584,24 +589,23 @@ const invertLogos = ['openai', 'blackforest', 'runway', 'ideogram', 'lightricks'
function ModelDropdown({ selectedModel, onSelect, onClose }) {
const [search, setSearch] = useState("");
const selectedEntry = imageModelPickerEntryByVariantId.get(selectedModel);
const selectedMode = imageModelCatalog.variantById.get(selectedModel)?.mode || "t2i";
const modelCategories = [
{
id: "all",
label: "All",
entries: [
...t2iModels.map((model) => ({ model, category: "t2i" })),
...i2iModels.map((model) => ({ model, category: "i2i" })),
],
entries: imageModelPickerEntries,
},
{
id: "t2i",
label: "Text to Image",
entries: t2iModels.map((model) => ({ model, category: "t2i" })),
entries: imageModelPickerEntries.filter((entry) => entry.variantsByMode.t2i),
},
{
id: "i2i",
label: "Image to Image",
entries: i2iModels.map((model) => ({ model, category: "i2i" })),
entries: imageModelPickerEntries.filter((entry) => entry.variantsByMode.i2i),
},
];
const [selectedCategory, setSelectedCategory] = useState("all");
@@ -658,31 +662,27 @@ function ModelDropdown({ selectedModel, onSelect, onClose }) {
const availableProviders = [];
const seenProviders = new Set();
modelEntries.forEach(({ model: m }) => {
const pId = m.provider || 'muapi';
const pName = m.provider_name || 'Muapi';
modelEntries.forEach(({ family }) => {
const pId = family.provider || 'muapi';
const pName = family.provider_name || 'Muapi';
if (!seenProviders.has(pId)) {
seenProviders.add(pId);
availableProviders.push({ id: pId, name: pName });
}
});
const filtered = modelEntries.filter(({ model: m }) => {
const filtered = modelEntries.filter((entry) => {
const { family } = entry;
// 1. Filter by provider tab
if (selectedProvider !== "all") {
const pId = m.provider || 'muapi';
const pId = family.provider || 'muapi';
if (pId !== selectedProvider) return false;
}
// 2. Filter by search query
const query = search.toLowerCase();
return (
m.name.toLowerCase().includes(query) ||
m.id.toLowerCase().includes(query)
);
return entry.searchText.includes(query);
});
const invertLogos = ['openai', 'blackforest', 'runway', 'ideogram', 'lightricks', 'grok'];
return (
<div className="flex gap-4 h-full max-h-[60vh] min-h-[350px] overflow-x-hidden">
{/* Left Sidebar: Provider tabs */}
@@ -710,9 +710,10 @@ function ModelDropdown({ selectedModel, onSelect, onClose }) {
key={p.id}
type="button"
onClick={() => setSelectedProvider(p.id)}
className={`w-8 h-8 flex-shrink-0 rounded-full flex items-center justify-center font-black text-[10px] border transition-all flex-shrink-0 cursor-pointer overflow-hidden ${
aria-pressed={isSelected}
className={`w-8 h-8 flex-shrink-0 rounded-full flex items-center justify-center overflow-hidden font-black text-[10px] border transition-all cursor-pointer ${
isSelected
? `${style.bg} border-white/25 scale-105 shadow-md`
? `${style.bg} scale-105 shadow-md shadow-black/10`
: "bg-white/[0.02] text-white/40 border-white/[0.02] hover:bg-white/5 hover:text-white/80"
}`}
title={p.name}
@@ -771,7 +772,11 @@ function ModelDropdown({ selectedModel, onSelect, onClose }) {
placeholder="Search models..."
value={search}
onClick={(e) => e.stopPropagation()}
onChange={(e) => setSearch(e.target.value)}
onChange={(e) => {
const value = e.target.value;
setSearch(value);
if (value.trim()) setSelectedProvider("all");
}}
className="bg-transparent border-none text-xs text-white focus:ring-0 w-full p-0 focus:outline-none"
/>
</div>
@@ -792,53 +797,62 @@ function ModelDropdown({ selectedModel, onSelect, onClose }) {
No models found
</div>
) : (
filtered.map(({ model: m, category }) => (
filtered.map((entry) => {
const { family } = entry;
const isSelected = selectedEntry === entry;
return (
<div
key={`${category}:${m.id}`}
ref={selectedModel === m.id ? activeItemRef : null}
key={entry.id}
ref={isSelected ? activeItemRef : null}
onClick={(e) => {
e.stopPropagation();
onSelect(m, category);
const variant = activeCategory.id === "all"
? entry.variantsByMode[selectedMode] || entry.defaultVariant
: entry.variantsByMode[activeCategory.id];
if (!variant) return;
onSelect(variant.model, variant.mode);
onClose();
}}
className={`flex items-center justify-between p-3 hover:bg-white/5 rounded-lg cursor-pointer transition-all border border-transparent hover:border-white/5 ${
selectedModel === m.id ? "bg-white/5 border-white/5" : ""
isSelected ? "bg-white/5 border-white/5" : ""
}`}
>
<div className="flex items-center gap-3">
{PROVIDER_LOGOS[m.provider] ? (
{PROVIDER_LOGOS[family.provider] ? (
<div className="w-8 h-8 rounded-full border border-white/5 overflow-hidden shrink-0 flex items-center justify-center bg-white/[0.02]">
<img
src={PROVIDER_LOGOS[m.provider]}
alt={m.provider_name}
className={`w-full h-full object-contain p-1 ${invertLogos.includes(m.provider) ? "invert" : ""}`}
src={PROVIDER_LOGOS[family.provider]}
alt={family.provider_name}
className={`w-full h-full object-contain p-1 ${invertLogos.includes(family.provider) ? "invert" : ""}`}
/>
</div>
) : (
<div
className={`w-8 h-8 ${
m.family === "kontext"
family.id.includes("kontext")
? "bg-blue-500/10 text-blue-400 border-blue-500/10"
: m.family === "effects"
: family.id.includes("effects")
? "bg-purple-500/10 text-purple-400 border-purple-500/10"
: "bg-primary/10 text-primary border-primary/10"
} border rounded-full flex items-center justify-center font-bold text-xs shadow-inner uppercase`}
>
{m.name.charAt(0)}
{entry.name.charAt(0)}
</div>
)}
<div className="flex flex-col gap-0.5 min-w-0">
<span className="text-xs font-bold text-white tracking-tight truncate">
{m.name}
{entry.name}
</span>
{selectedProvider === "all" && m.provider_name && (
<div className="flex items-center gap-1.5">
{selectedProvider === "all" && family.provider_name && (
<span className="text-[9px] text-white/40">
{m.provider_name}
{family.provider_name}
</span>
)}
</div>
</div>
</div>
{selectedModel === m.id && (
{isSelected && (
<svg
width="14"
height="14"
@@ -851,7 +865,8 @@ function ModelDropdown({ selectedModel, onSelect, onClose }) {
</svg>
)}
</div>
))
);
})
)}
</div>
</div>
@@ -907,6 +922,8 @@ export default function ImageStudio({
const [imageMode, setImageMode] = useState(false); // false=t2i, true=i2i
const [selectedModelId, setSelectedModelId] = useState(t2iModels[0].id);
const [selectedModelName, setSelectedModelName] = useState(t2iModels[0].name);
const selectedModelDisplayName =
imageModelPickerEntryByVariantId.get(selectedModelId)?.name || selectedModelName;
const [selectedAr, setSelectedAr] = useState(
t2iModels[0].inputs?.aspect_ratio?.default || "1:1",
);
@@ -1493,7 +1510,7 @@ export default function ImageStudio({
<h1 className="text-2xl sm:text-4xl md:text-5xl font-extrabold tracking-tight mb-4 text-center px-4 flex flex-col items-center">
<span className="text-white font-black uppercase text-xl sm:text-3xl tracking-wide mb-1 opacity-90">START CREATING WITH</span>
<span className="text-[#22d3ee] font-black uppercase text-2xl sm:text-4xl sm:mt-1 tracking-tight">
{selectedModelName}
{selectedModelDisplayName}
</span>
</h1>
<p className="text-white/40 text-xs sm:text-sm font-medium tracking-wide text-center max-w-lg leading-relaxed px-4">
@@ -1593,7 +1610,7 @@ export default function ImageStudio({
})()}
</div>
<span className={PROMPT_CONTROL_LABEL_CLASS}>
{selectedModelName}
{selectedModelDisplayName}
</span>
<PromptChevronIcon />
</button>
+65 -69
View File
@@ -24,6 +24,11 @@ import {
getModesForModel,
getMaxImagesForI2VModel,
} from "../models.js";
import {
videoModelCatalog,
videoModelPickerEntries,
videoModelPickerEntryByVariantId,
} from "../modelFamilies.js";
import {
PROMPT_CONTROL_LABEL_CLASS,
PROMPT_MEDIA_PREVIEW_CLASS,
@@ -148,34 +153,35 @@ const invertLogos = ['openai', 'blackforest', 'runway', 'ideogram', 'lightricks'
function ModelDropdown({ selectedModel, onSelect, onClose }) {
const [search, setSearch] = useState("");
const selectedEntry = videoModelPickerEntryByVariantId.get(selectedModel);
const selectedMode = videoModelCatalog.variantById.get(selectedModel)?.mode || "t2v";
const selectedModelProvider = selectedEntry?.family.provider || "all";
const modelCategories = [
{
id: "all",
label: "All",
entries: [
...t2vModels.map((model) => ({ model, category: "t2v" })),
...i2vModels.map((model) => ({ model, category: "i2v" })),
...v2vModels.map((model) => ({ model, category: "v2v" })),
],
entries: videoModelPickerEntries,
},
{
id: "t2v",
label: "Text to Video",
entries: t2vModels.map((model) => ({ model, category: "t2v" })),
entries: videoModelPickerEntries.filter((entry) => entry.variantsByMode.t2v),
},
{
id: "i2v",
label: "Image to Video",
entries: i2vModels.map((model) => ({ model, category: "i2v" })),
entries: videoModelPickerEntries.filter((entry) => entry.variantsByMode.i2v),
},
{
id: "v2v",
label: "Video Tools",
entries: v2vModels.map((model) => ({ model, category: "v2v" })),
entries: videoModelPickerEntries.filter((entry) => entry.variantsByMode.v2v),
},
];
const [selectedCategory, setSelectedCategory] = useState("all");
const [selectedProvider, setSelectedProvider] = useState("all");
const [selectedProvider, setSelectedProvider] = useState(
() => selectedModelProvider,
);
const activeCategory = modelCategories.find((category) => category.id === selectedCategory) || modelCategories[0];
const modelEntries = activeCategory.entries;
@@ -228,9 +234,9 @@ function ModelDropdown({ selectedModel, onSelect, onClose }) {
const availableProviders = [];
const seenProviders = new Set();
modelEntries.forEach(({ model: m }) => {
const pId = m.provider || 'muapi';
const pName = m.provider_name || 'Muapi';
modelEntries.forEach(({ family }) => {
const pId = family.provider || 'muapi';
const pName = family.provider_name || 'Muapi';
if (!seenProviders.has(pId)) {
seenProviders.add(pId);
availableProviders.push({ id: pId, name: pName });
@@ -239,83 +245,76 @@ function ModelDropdown({ selectedModel, onSelect, onClose }) {
const lf = search.toLowerCase();
const filterFn = ({ model: m }) => {
const filtered = modelEntries.filter((entry) => {
const { family } = entry;
// 1. Filter by provider tab
if (selectedProvider !== "all") {
const pId = m.provider || 'muapi';
const pId = family.provider || 'muapi';
if (pId !== selectedProvider) return false;
}
// 2. Filter by search query
return (
m.name.toLowerCase().includes(lf) ||
m.id.toLowerCase().includes(lf)
);
};
return entry.searchText.includes(lf);
});
const filteredMain = modelEntries.filter(filterFn).filter(({ category }) => category !== "v2v");
const filteredV2V = modelEntries.filter(filterFn).filter(({ category }) => category === "v2v");
const getIconColor = (m, isV2V) => {
if (isV2V) return "bg-orange-500/10 text-orange-400 border-orange-500/10";
if (m.id.includes("kling")) return "bg-blue-500/10 text-blue-400 border-blue-500/10";
if (m.id.includes("veo")) return "bg-purple-500/10 text-purple-400 border-purple-500/10";
if (m.id.includes("sora")) return "bg-rose-500/10 text-rose-400 border-rose-500/10";
const getIconColor = (family) => {
if (family.id.includes("kling")) return "bg-blue-500/10 text-blue-400 border-blue-500/10";
if (family.id.includes("veo")) return "bg-purple-500/10 text-purple-400 border-purple-500/10";
if (family.id.includes("sora")) return "bg-rose-500/10 text-rose-400 border-rose-500/10";
return "bg-primary/10 text-primary border-primary/10";
};
const renderItem = ({ model: m, category }) => {
const isV2V = category === "v2v";
const renderItem = (entry) => {
const { family } = entry;
const isSelected = selectedEntry === entry;
return (
<div
key={`${category}:${m.id}`}
ref={selectedModel === m.id ? activeItemRef : null}
className={`flex items-center justify-between p-3.5 hover:bg-white/5 rounded-2xl cursor-pointer transition-all border border-transparent hover:border-white/5 ${selectedModel === m.id ? "bg-white/5 border-white/5" : ""}`}
key={entry.id}
ref={isSelected ? activeItemRef : null}
className={`flex items-center justify-between p-3.5 hover:bg-white/5 rounded-2xl cursor-pointer transition-all border border-transparent hover:border-white/5 ${isSelected ? "bg-white/5 border-white/5" : ""}`}
onClick={(e) => {
e.stopPropagation();
onSelect(m, category);
const variant = activeCategory.id === "all"
? entry.variantsByMode[selectedMode] || entry.defaultVariant
: entry.variantsByMode[activeCategory.id];
if (!variant) return;
onSelect(variant.model, variant.mode);
onClose();
}}
>
<div className="flex items-center gap-3.5">
{PROVIDER_LOGOS[m.provider] ? (
{PROVIDER_LOGOS[family.provider] ? (
<div className="w-8 h-8 rounded-xl border border-white/5 overflow-hidden shrink-0 flex items-center justify-center bg-white/[0.02]">
<img
src={PROVIDER_LOGOS[m.provider]}
alt={m.provider_name}
className={`w-full h-full object-contain p-1 ${invertLogos.includes(m.provider) ? "invert" : ""}`}
src={PROVIDER_LOGOS[family.provider]}
alt={family.provider_name}
className={`w-full h-full object-contain p-1 ${invertLogos.includes(family.provider) ? "invert" : ""}`}
/>
</div>
) : (
<div
className={`w-9 h-9 ${getIconColor(m, isV2V)} border rounded-xl flex items-center justify-center font-black text-xs shadow-inner uppercase`}
className={`w-9 h-9 ${getIconColor(family)} border rounded-xl flex items-center justify-center font-black text-xs shadow-inner uppercase`}
>
{m.name.charAt(0)}
{entry.name.charAt(0)}
</div>
)}
<div className="flex flex-col gap-0.5 min-w-0">
<span className="text-xs font-bold text-white tracking-tight truncate">
{m.name}
{entry.name}
</span>
{isV2V ? (
<span className="text-[9px] text-orange-400/70">
{m.imageField ? "Upload a video and image" : "Upload a video to use"}
</span>
) : (
selectedProvider === "all" && m.provider_name && (
<div className="flex items-center gap-1.5">
{selectedProvider === "all" && family.provider_name && (
<span className="text-[9px] text-white/40">
{m.provider_name}
{family.provider_name}
</span>
)
)}
)}
</div>
</div>
</div>
{selectedModel === m.id && <CheckSvg />}
{isSelected && <CheckSvg />}
</div>
);
};
const invertLogos = ['openai', 'blackforest', 'runway', 'ideogram', 'lightricks', 'grok'];
return (
<div className="flex gap-4 h-full max-h-[70vh] min-h-[350px]">
{/* Left Sidebar: Provider tabs */}
@@ -343,9 +342,10 @@ function ModelDropdown({ selectedModel, onSelect, onClose }) {
key={p.id}
type="button"
onClick={() => setSelectedProvider(p.id)}
className={`w-8 h-8 flex-shrink-0 rounded-full flex items-center justify-center font-black text-[10px] border transition-all flex-shrink-0 cursor-pointer overflow-hidden ${
aria-pressed={isSelected}
className={`w-8 h-8 flex-shrink-0 rounded-full flex items-center justify-center overflow-hidden font-black text-[10px] border transition-all cursor-pointer ${
isSelected
? `${style.bg} border-white/25 scale-105 shadow-md`
? `${style.bg} scale-105 shadow-md shadow-black/10`
: "bg-white/[0.02] text-white/40 border-white/[0.02] hover:bg-white/5 hover:text-white/80"
}`}
title={p.name}
@@ -403,7 +403,11 @@ function ModelDropdown({ selectedModel, onSelect, onClose }) {
type="text"
placeholder="Search models..."
value={search}
onChange={(e) => setSearch(e.target.value)}
onChange={(e) => {
const value = e.target.value;
setSearch(value);
if (value.trim()) setSelectedProvider("all");
}}
onClick={(e) => e.stopPropagation()}
className="bg-transparent border-none text-xs text-white focus:ring-0 w-full p-0 outline-none"
/>
@@ -420,22 +424,12 @@ function ModelDropdown({ selectedModel, onSelect, onClose }) {
</div>
<div className="flex flex-col gap-1.5 overflow-y-auto custom-scrollbar pr-1 pb-2 flex-1">
{filteredMain.length === 0 && filteredV2V.length === 0 ? (
{filtered.length === 0 ? (
<div className="text-xs text-white/30 text-center py-6">
No models found
</div>
) : (
<>
{filteredMain.map((entry) => renderItem(entry))}
{filteredV2V.length > 0 && (
<>
<div className="text-xs font-bold text-orange-400/70 px-3 py-2 mt-1 border-t border-white/5">
Video Tools
</div>
{filteredV2V.map((entry) => renderItem(entry))}
</>
)}
</>
filtered.map((entry) => renderItem(entry))
)}
</div>
</div>
@@ -475,6 +469,8 @@ export default function VideoStudio({
const defaultModel = t2vModels[0];
const [selectedModel, setSelectedModel] = useState(defaultModel.id);
const [selectedModelName, setSelectedModelName] = useState(defaultModel.name);
const selectedModelDisplayName =
videoModelPickerEntryByVariantId.get(selectedModel)?.name || selectedModelName;
const [selectedAr, setSelectedAr] = useState(
defaultModel.inputs?.aspect_ratio?.default || "16:9",
);
@@ -1605,7 +1601,7 @@ export default function VideoStudio({
<h1 className="text-2xl sm:text-4xl md:text-5xl font-extrabold tracking-tight mb-4 text-center px-4 flex flex-col items-center">
<span className="text-white font-black uppercase text-xl sm:text-3xl tracking-wide mb-1 opacity-90">START CREATING WITH</span>
<span className="text-[#22d3ee] font-black uppercase text-2xl sm:text-4xl sm:mt-1 tracking-tight">
{selectedModelName}
{selectedModelDisplayName}
</span>
</h1>
<p className="text-white/40 text-xs sm:text-sm font-medium tracking-wide text-center max-w-lg leading-relaxed px-4">
@@ -1940,7 +1936,7 @@ export default function VideoStudio({
})()}
</div>
<span className={PROMPT_CONTROL_LABEL_CLASS}>
{selectedModelName}
{selectedModelDisplayName}
</span>
<PromptChevronIcon />
</button>
+662
View File
@@ -0,0 +1,662 @@
import {
i2iModels,
i2vModels,
t2iModels,
t2vModels,
v2vModels,
} from "./models.js";
const IMAGE_FAMILY_ALIASES = {
"bytedance-seededit-v3": "bytedance-seedream-v3",
"bytedance-seedream-edit-v4": "bytedance-seedream-v4",
"bytedance-seedream-v4-edit": "bytedance-seedream-v4",
"bytedance-seedream-v5.0": "seedream-5.0",
"bytedance-seedream-v5.0-edit": "seedream-5.0",
"seedream-5.0-edit": "seedream-5.0",
"bytedance-seedream-5.0-pro": "seedream-5.0",
"bytedance-seedream-5.0-pro-edit": "seedream-5.0",
"google-imagen4-fast": "google-imagen4",
"google-imagen4-ultra": "google-imagen4",
"flux-2-klein-4b": "flux-2-klein",
"flux-2-klein-9b": "flux-2-klein",
"flux-2-klein-4b-turbo": "flux-2-klein",
"flux-2-klein-9b-turbo": "flux-2-klein",
"flux-2-klein-4b-text-to-image-lora": "flux-2-klein",
"flux-2-klein-9b-text-to-image-lora": "flux-2-klein",
"flux-2-klein-4b-edit": "flux-2-klein",
"flux-2-klein-9b-edit": "flux-2-klein",
"flux-2-klein-4b-turbo-edit": "flux-2-klein",
"flux-2-klein-9b-turbo-edit": "flux-2-klein",
"flux-2-klein-4b-edit-lora": "flux-2-klein",
"flux-2-klein-9b-edit-lora": "flux-2-klein",
"flux-dev-lora": "flux-dev",
"flux-2-dev-edit": "flux-2-dev",
"flux-2-flex-edit": "flux-2-flex",
"flux-2-pro-edit": "flux-2-pro",
"gpt-image-1.5-edit": "gpt-image-1.5",
"gpt-image-2-edit": "gpt-image-2",
"gpt4o-image-to-image": "gpt4o",
"gpt4o-text-to-image": "gpt4o",
"grok-imagine-image-to-image": "grok-imagine",
"grok-imagine-text-to-image": "grok-imagine",
"grok-imagine-text-to-image-quality": "grok-imagine",
"ideogram-v3-reframe": "ideogram-v3",
"ideogram-v3-t2i": "ideogram-v3",
"kling-o1-edit-image": "kling-o1",
"kling-o1-text-to-image": "kling-o1",
"kling-o3-image": "kling-o3",
"kling-o3-image-edit": "kling-o3",
"midjourney-v7-image-to-image": "midjourney-v7-text-to-image",
"midjourney-v7-omni-reference": "midjourney-v7",
"midjourney-v7-style-reference": "midjourney-v7",
"midjourney-v7-text-to-image": "midjourney-v7-text-to-image",
"minimax-image-01-subject-reference": "minimax-image-01",
"nano-banana-edit": "nano-banana",
"nano-banana-2-edit": "nano-banana-2",
"nano-banana-2-lite-edit": "nano-banana-2-lite",
"nano-banana-effects": "nano-banana",
"nano-banana-pro-edit": "nano-banana-pro",
"qwen-image-edit": "qwen-image",
"qwen-image-2.0-pro": "qwen-image-2.0",
"qwen-image-2.0-edit": "qwen-image-2.0",
"qwen-image-2.0-pro-edit": "qwen-image-2.0",
"qwen-image-edit-plus": "qwen-plus",
"qwen-image-edit-plus-lora": "qwen-plus",
"qwen3-pro-text-to-image": "qwen3",
"qwen3-pro-image-to-image": "qwen3",
"reve-image-edit": "reve",
"reve-text-to-image": "reve",
"bytedance-seedream-v4.5-edit": "bytedance-seedream-v4.5",
"vidu-q2-reference-to-image": "vidu-q2",
"vidu-q2-text-to-image": "vidu-q2",
"wan2.7-image-edit-pro": "wan2.7",
"wan2.7-text-to-image-pro": "wan2.7",
};
const IMAGE_FAMILY_NAMES = {
"flux-2-klein": "Flux 2 Klein",
"flux-dev": "Flux Dev",
gpt4o: "GPT-4o",
"grok-imagine": "Grok Imagine",
"google-imagen4": "Google Imagen 4",
"kling-o3": "Kling O3",
"seedream-5.0": "Seedream 5.0",
"qwen-image-2.0": "Qwen Image 2.0",
"qwen-plus": "Qwen Image Edit Plus",
qwen3: "Qwen 3",
"wan2.7": "Wan 2.7",
};
const VIDEO_FAMILY_ALIASES = {
"ai-video-upscaler-pro": "ai-video-upscaler",
"hunyuan-fast-text-to-video": "hunyuan",
"hunyuan-image-to-video": "hunyuan",
"hunyuan-text-to-video": "hunyuan",
"video-effects": "ai-video-effects",
};
const VIDEO_FAMILY_NAMES = {
"ai-video-upscaler": "AI Video Upscaler",
hunyuan: "Hunyuan",
};
function videoVariantKey(model) {
const endpoint = model.endpoint || model.id;
const effectOptions = model.inputs?.name?.enum;
return effectOptions?.length
? `${endpoint}\u0000${effectOptions.join("\u0000")}`
: endpoint;
}
function normalizeVersion(value) {
return value.replace(/^v/i, "").replace(/\.0$/, "");
}
function imageFamilyId(model) {
return (IMAGE_FAMILY_ALIASES[model.id] || model.id)
.toLowerCase()
.replace(/-(text-to-image|image-to-image|image-edit)(?=-|$)/g, "")
.replace(/-(t2i|i2i)(?=-|$)/g, "")
.replace(/-edit(?=-|$)/g, "")
.replace(/--+/g, "-")
.replace(/^-|-$/g, "");
}
function videoFamilyId(model) {
const modelId = model.id.toLowerCase();
const id = VIDEO_FAMILY_ALIASES[modelId] || modelId;
let match = id.match(/^kling-(v?\d+(?:\.\d+)?|o1)(?:-|$)/);
if (match) return `kling-${match[1].toLowerCase() === "o1" ? "o1" : `v${normalizeVersion(match[1])}`}`;
match = id.match(/^seedance-(v?\d+(?:\.\d+)?|lite|pro)(?:-|$)/);
if (match) return `seedance-${normalizeVersion(match[1])}`;
match = id.match(/^wan(\d+(?:\.\d+)?)(?:-|$)/);
if (match) return `wan-${normalizeVersion(match[1])}`;
match = id.match(/^veo-?(\d+(?:\.\d+)?)(?:-|$)/);
if (match) return `veo-${normalizeVersion(match[1])}`;
match = id.match(/^pixverse-v?(\d+(?:\.\d+)?)(?:-|$)/);
if (match) return `pixverse-${normalizeVersion(match[1])}`;
match = id.match(/^vidu-(v?\d+(?:\.\d+)?|q\d+)(?:-|$)/);
if (match) return `vidu-${normalizeVersion(match[1])}`;
match = id.match(/^ltx-(\d+(?:\.\d+)?)(?:-|$)/);
if (match) return `ltx-${normalizeVersion(match[1])}`;
match = id.match(/^happy-horse-(\d+(?:\.\d+)?)(?:-|$)/);
if (match) return `happy-horse-${normalizeVersion(match[1])}`;
match = id.match(/^minimax-hailuo-(\d+(?:\.\d+)?)(?:-|$)/);
if (match) return `minimax-hailuo-${normalizeVersion(match[1])}`;
if (id.startsWith("minimax-h3")) return "minimax-h3";
if (id.startsWith("openai-sora-2")) return "sora-2";
if (id.startsWith("grok-imagine")) return "grok-imagine-video";
if (id.startsWith("gemini-omni")) return "gemini-omni";
return id
.replace(/-(text-to-video|image-to-video|reference-to-video)(?=-|$)/g, "")
.replace(/-(t2v|i2v|v2v)(?=-|$)/g, "")
.replace(/--+/g, "-")
.replace(/^-|-$/g, "");
}
function cleanImageFamilyName(name) {
return name
.replace(/\s+(Text To Image|Image To Image|Text to Image|Image to Image|T2I|I2I)$/i, "")
.replace(/\s+Edit$/i, "")
.trim();
}
function videoFamilyName(familyId, fallbackName) {
const versionedNames = [
[/^kling-v(.+)$/, "Kling v"],
[/^seedance-(.+)$/, "Seedance "],
[/^wan-(.+)$/, "Wan "],
[/^veo-(.+)$/, "Veo "],
[/^pixverse-(.+)$/, "Pixverse v"],
[/^vidu-(.+)$/, "Vidu "],
[/^ltx-(.+)$/, "LTX "],
[/^happy-horse-(.+)$/, "Happy Horse "],
[/^minimax-hailuo-(.+)$/, "Hailuo "],
];
for (const [pattern, prefix] of versionedNames) {
const match = familyId.match(pattern);
if (match) return `${prefix}${match[1].replace(/^q/i, "Q")}`;
}
const names = {
"gemini-omni": "Gemini Omni",
"grok-imagine-video": "Grok Imagine",
"kling-o1": "Kling O1",
"minimax-h3": "MiniMax H3",
"sora-2": "Sora 2",
};
return VIDEO_FAMILY_NAMES[familyId] || names[familyId] || fallbackName;
}
function videoSeriesVersion(family) {
const match = family.id.match(
/^(kling-v|seedance-|wan-|veo-|pixverse-|vidu-q|ltx-|happy-horse-|minimax-hailuo-)(\d+(?:\.\d+)?)/,
);
return match ? { series: match[1], version: Number(match[2]) } : null;
}
function imageSeriesVersion(family) {
const patterns = [
[/^gpt-image-(\d+(?:\.\d+)?)/, "gpt-image"],
[/^hunyuan-image-(\d+(?:\.\d+)?)/, "hunyuan-image"],
[/^midjourney-v(\d+(?:\.\d+)?)/, "midjourney"],
[/^qwen-image-(\d+(?:\.\d+)?)/, "qwen-image"],
[/^wan(\d+(?:\.\d+)?)/, "wan-image"],
[/^(?:bytedance-)?seedream-v?(\d+(?:\.\d+)?)/, "seedream-image"],
];
for (const [pattern, series] of patterns) {
const match = family.id.match(pattern);
if (match) return { series, version: Number(match[1]) };
}
return null;
}
function stripFamilyPrefix(name, family) {
const patterns = {
"grok-imagine-video": /^Grok Imagine\s*/i,
"minimax-h3": /^MiniMax H3\s*/i,
"sora-2": /^(?:Openai\s+)?Sora 2\s*/i,
};
if (patterns[family.id]) return name.replace(patterns[family.id], "");
if (family.id === "kling-v3") return name.replace(/^Kling\s+v?3(?:\.0)?\s*/i, "");
const versionPatterns = [
[/^kling-v(.+)$/, (version) => new RegExp(`^Kling\\s+v?${version.replace(".", "\\.")}(?:\\.0)?\\s*`, "i")],
[/^seedance-(.+)$/, (version) => new RegExp(`^Seedance\\s+v?${version.replace(".", "\\.")}(?:\\.0)?\\s*`, "i")],
[/^wan-(.+)$/, (version) => new RegExp(`^Wan\\s*${version.replace(".", "\\.")}\\s*`, "i")],
[/^veo-(.+)$/, (version) => new RegExp(`^Veo\\s*${version.replace(".", "\\.")}\\s*`, "i")],
[/^pixverse-(.+)$/, (version) => new RegExp(`^Pixverse\\s+v?${version.replace(".", "\\.")}\\s*`, "i")],
[/^vidu-(.+)$/, (version) => new RegExp(`^Vidu\\s+${version.replace(/^q/i, "Q").replace(".", "\\.")}\\s*`, "i")],
[/^ltx-(.+)$/, (version) => new RegExp(`^LTX\\s+${version.replace(".", "\\.")}\\s*`, "i")],
[/^happy-horse-(.+)$/, (version) => new RegExp(`^Happy Horse\\s+${version.replace(".", "\\.")}\\s*`, "i")],
[/^minimax-hailuo-(.+)$/, (version) => new RegExp(`^(?:MiniMax\\s+)?Hailuo\\s+${version.replace(".", "\\.")}\\s*`, "i")],
];
for (const [familyPattern, createNamePattern] of versionPatterns) {
const match = family.id.match(familyPattern);
if (match) return name.replace(createNamePattern(match[1]), "");
}
const escapedName = family.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
return name.replace(new RegExp(`^${escapedName}\\s*`, "i"), "");
}
function buildCatalog(modeLists, config) {
const sourceEntries = [];
const familyParents = new Map();
const endpointOwners = new Map();
const findFamily = (id) => {
let root = id;
while (familyParents.get(root) !== root) root = familyParents.get(root);
let current = id;
while (familyParents.get(current) !== current) {
const next = familyParents.get(current);
familyParents.set(current, root);
current = next;
}
return root;
};
const unionFamilies = (ownerId, duplicateId) => {
const ownerRoot = findFamily(ownerId);
const duplicateRoot = findFamily(duplicateId);
if (ownerRoot !== duplicateRoot) familyParents.set(duplicateRoot, ownerRoot);
};
modeLists.forEach(({ mode, models }) => {
const denominator = Math.max(models.length - 1, 1);
models.forEach((model, index) => {
const id = config.familyId(model);
if (!familyParents.has(id)) familyParents.set(id, id);
const endpoint = model.endpoint || model.id;
const endpointOwner = endpointOwners.get(endpoint);
if (endpointOwner) unionFamilies(endpointOwner, id);
else endpointOwners.set(endpoint, id);
sourceEntries.push({ model, mode, freshness: index / denominator, familyId: id });
});
});
const familyMap = new Map();
sourceEntries.forEach((entry) => {
const id = findFamily(entry.familyId);
entry.familyId = id;
const { model, mode, freshness } = entry;
const family = familyMap.get(id) || {
id,
name: "",
provider: model.provider || "muapi",
provider_name: model.provider_name || "Muapi",
rawVariants: {},
freshness: 0,
};
(family.rawVariants[mode] ||= []).push(entry);
family.freshness = Math.max(family.freshness, freshness);
familyMap.set(id, family);
});
const families = [...familyMap.values()].map((family) => {
const preferredMode = config.namingModes.find((mode) => family.rawVariants[mode]?.length);
const namingEntry = [...family.rawVariants[preferredMode]].sort(
(a, b) => b.freshness - a.freshness,
)[0];
family.name = config.familyName(family.id, namingEntry.model.name);
family.provider = namingEntry.model.provider || family.provider;
family.provider_name = namingEntry.model.provider_name || family.provider_name;
family.variants = {};
family.supports = {};
modeLists.forEach(({ mode }) => {
const seenIds = new Set();
const seenEndpoints = new Set();
const variants = (family.rawVariants[mode] || [])
.sort((a, b) => b.freshness - a.freshness)
.filter(({ model }) => {
if (seenIds.has(model.id)) return false;
seenIds.add(model.id);
const endpoint = config.variantKey?.(model) || model.endpoint || model.id;
if (seenEndpoints.has(endpoint)) return false;
seenEndpoints.add(endpoint);
return true;
})
.map((entry) => ({ ...entry }));
family.variants[mode] = variants;
family.supports[mode] = variants.length > 0;
});
family.searchText = [
family.id,
family.name,
...Object.values(family.rawVariants).flatMap((variants) =>
variants.flatMap(({ model }) => [model.id, model.name]),
),
]
.join(" ")
.toLowerCase();
delete family.rawVariants;
return family;
});
families.forEach((family) => {
family.sortFreshness = family.freshness;
});
if (config.seriesVersion) {
const seriesGroups = new Map();
families.forEach((family) => {
const versionInfo = config.seriesVersion(family);
if (!versionInfo) return;
let entries = seriesGroups.get(versionInfo.series);
if (!entries) {
entries = [];
seriesGroups.set(versionInfo.series, entries);
}
entries.push({ family, version: versionInfo.version });
});
seriesGroups.forEach((entries) => {
const freshnessSlots = entries
.map(({ family }) => family.freshness)
.sort((a, b) => b - a);
entries
.sort(
(a, b) =>
b.version - a.version || b.family.freshness - a.family.freshness,
)
.forEach(({ family }, index) => {
family.sortFreshness = freshnessSlots[index];
});
});
}
families.sort(
(a, b) => b.sortFreshness - a.sortFreshness || a.name.localeCompare(b.name),
);
const familyById = new Map(families.map((family) => [family.id, family]));
const familyByVariantId = new Map();
const variantById = new Map();
families.forEach((family) => {
Object.values(family.variants).forEach((variants) => {
variants.forEach((variant) => {
familyByVariantId.set(variant.model.id, family);
variantById.set(variant.model.id, variant);
});
});
});
sourceEntries.forEach((entry) => {
const family = familyById.get(entry.familyId);
familyByVariantId.set(entry.model.id, family);
if (!variantById.has(entry.model.id)) {
variantById.set(entry.model.id, { ...entry });
}
});
return {
families,
familyById,
familyByVariantId,
variantById,
};
}
export const imageModelCatalog = buildCatalog(
[
{ mode: "t2i", models: t2iModels },
{ mode: "i2i", models: i2iModels },
],
{
familyId: imageFamilyId,
familyName: (id, fallback) => IMAGE_FAMILY_NAMES[id] || cleanImageFamilyName(fallback),
namingModes: ["t2i", "i2i"],
seriesVersion: imageSeriesVersion,
},
);
export const videoModelCatalog = buildCatalog(
[
{ mode: "t2v", models: t2vModels },
{ mode: "i2v", models: i2vModels },
{ mode: "v2v", models: v2vModels },
],
{
familyId: videoFamilyId,
familyName: videoFamilyName,
namingModes: ["t2v", "i2v", "v2v"],
seriesVersion: videoSeriesVersion,
variantKey: videoVariantKey,
},
);
const PICKER_INPUT_SUFFIX = /\s+(?:Text|Image)\s+To\s+(?:Image|Video)$/i;
const PICKER_INPUT_ID_SUFFIX = /-(?:text-to-image|image-to-image|text-to-video|image-to-video|reference-to-video|t2i|i2i|t2v|i2v|v2v)$/;
const PICKER_MEDIA_FIELDS = new Set([
"image_url",
"images_list",
"image_urls",
"reference_images",
"start_image_url",
"first_image_url",
"last_image",
"last_image_url",
"end_image_url",
"video_url",
"videos_list",
"video_urls",
"reference_videos",
"audio_url",
"audios_list",
"audio_urls",
"reference_audios",
]);
const PICKER_SCHEMA_METADATA = new Set(["name", "title", "description", "examples"]);
function normalizePickerName(value) {
return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
}
function pickerBaseName(model, isAlias) {
const name = model.name.replace(PICKER_INPUT_SUFFIX, "").trim();
return isAlias ? name.replace(/\s+Edit$/i, "").trim() : name;
}
function pickerDisplayName(model, family, isAlias) {
const baseName = pickerBaseName(model, isAlias);
const suffix = stripFamilyPrefix(baseName, family).trim();
return suffix !== baseName
? (suffix ? `${family.name} ${suffix}` : family.name)
: baseName;
}
function aliasProfile(modelId, alias) {
const ignoredTokens = new Set([
"bytedance",
"edit",
"image",
"reference",
"text",
"to",
"video",
"t2i",
"i2i",
"t2v",
"i2v",
"v2v",
]);
const tokens = (value) => value
.toLowerCase()
.replace(/\bv(?=\d)/g, "")
.split(/[^a-z0-9]+/)
.filter(Boolean);
const aliasTokens = new Set(tokens(alias));
return tokens(modelId)
.filter((token) => !aliasTokens.has(token) && !ignoredTokens.has(token))
.join("-");
}
function formatAliasProfile(profile) {
const special = { lora: "LoRA" };
return profile
.split("-")
.filter(Boolean)
.map((token) => special[token] || (/^\d+b$/.test(token)
? token.toUpperCase()
: token[0].toUpperCase() + token.slice(1)))
.join(" ");
}
function normalizePickerSchema(value) {
if (Array.isArray(value)) return value.map(normalizePickerSchema);
if (!value || typeof value !== "object") return value;
return Object.fromEntries(
Object.keys(value)
.filter((key) => !PICKER_SCHEMA_METADATA.has(key))
.sort()
.map((key) => [key, normalizePickerSchema(value[key])]),
);
}
function pickerContractKey(model) {
const inputs = Object.fromEntries(
Object.entries(model.inputs || {})
.filter(
([name, input]) =>
!PICKER_MEDIA_FIELDS.has(name) &&
!["image", "video", "audio"].includes(input?.field),
)
.map(([name, input]) => [name, normalizePickerSchema(input)]),
);
return JSON.stringify({
hasPrompt: model.hasPrompt !== false,
promptRequired: !!model.promptRequired,
inputs: normalizePickerSchema(inputs),
});
}
function pickerIdentity(model, aliases, aliasTargets) {
const alias = aliases[model.id] || (aliasTargets.has(model.id) ? model.id : null);
if (alias) {
const profile = aliasProfile(model.id, alias);
return { key: `alias:${alias}:${profile}`, profile };
}
const inputStem = model.id.toLowerCase().replace(PICKER_INPUT_ID_SUFFIX, "");
if (inputStem !== model.id.toLowerCase()) {
return { key: `input:${inputStem}:${pickerContractKey(model)}`, profile: null };
}
return { key: `model:${model.id}`, profile: null };
}
function buildModelPickerEntries(catalog, modes, aliases) {
const entries = [];
const aliasTargets = new Set(Object.values(aliases));
for (const family of catalog.families) {
const familyEntries = new Map();
for (const mode of modes) {
for (const variant of family.variants[mode] || []) {
const identity = pickerIdentity(variant.model, aliases, aliasTargets);
const isAlias = identity.key.startsWith("alias:");
let entry = familyEntries.get(identity.key);
if (!entry) {
entry = {
id: `${family.id}:${variant.model.id}`,
family,
name: isAlias
? [family.name, formatAliasProfile(identity.profile)].filter(Boolean).join(" ")
: pickerDisplayName(variant.model, family, false),
variantIds: new Set(),
variantsByMode: {},
};
familyEntries.set(identity.key, entry);
}
entry.variantIds.add(variant.model.id);
entry.variantsByMode[mode] ||= variant;
}
}
const nameCounts = new Map();
for (const entry of familyEntries.values()) {
const key = normalizePickerName(entry.name);
nameCounts.set(key, (nameCounts.get(key) || 0) + 1);
}
for (const entry of familyEntries.values()) {
entry.defaultVariant = modes
.map((mode) => entry.variantsByMode[mode])
.find(Boolean) || null;
const duplicateName = nameCounts.get(normalizePickerName(entry.name)) > 1;
if (duplicateName) {
const entryModes = Object.keys(entry.variantsByMode);
if (entryModes.length === 1 && ["i2i", "i2v"].includes(entryModes[0])) {
entry.name = `${entry.name} Image`;
} else if (entryModes.length === 1 && entryModes[0] === "v2v") {
entry.name = `${entry.name} Video`;
} else {
entry.name = entry.defaultVariant.model.name;
}
}
if (normalizePickerName(entry.name) === normalizePickerName(family.name)) {
entry.name = family.name;
}
entry.searchText = `${family.searchText} ${entry.name}`.toLowerCase();
entries.push(Object.freeze(entry));
}
}
return Object.freeze(entries);
}
export const imageModelPickerEntries = buildModelPickerEntries(
imageModelCatalog,
["t2i", "i2i"],
IMAGE_FAMILY_ALIASES,
);
export const videoModelPickerEntries = buildModelPickerEntries(
videoModelCatalog,
["t2v", "i2v", "v2v"],
VIDEO_FAMILY_ALIASES,
);
function indexModelPickerEntries(entries, catalog) {
const entryByVariantId = new Map();
const entryByModeEndpoint = new Map();
for (const entry of entries) {
for (const variantId of entry.variantIds) {
entryByVariantId.set(variantId, entry);
}
for (const [mode, variant] of Object.entries(entry.variantsByMode)) {
const endpoint = variant.model.endpoint || variant.model.id;
entryByModeEndpoint.set(`${mode}\u0000${endpoint}`, entry);
}
}
for (const [variantId, variant] of catalog.variantById) {
if (entryByVariantId.has(variantId)) continue;
const endpoint = variant.model.endpoint || variant.model.id;
const entry = entryByModeEndpoint.get(`${variant.mode}\u0000${endpoint}`);
if (entry) entryByVariantId.set(variantId, entry);
}
return entryByVariantId;
}
export const imageModelPickerEntryByVariantId = indexModelPickerEntries(
imageModelPickerEntries,
imageModelCatalog,
);
export const videoModelPickerEntryByVariantId = indexModelPickerEntries(
videoModelPickerEntries,
videoModelCatalog,
);