some "optimization"? not convinced this is better

This commit is contained in:
Brandon Thomas committed 2026-07-01 22:09:35 -04:00
1 parent 5ca570e8af
commit 690d00aa18
8 files changed
+1236 -1221

No files matched your search

@@ -14,6 +14,12 @@ import { GenerationProvider } from "@storyteller/api-enums";
import { Tooltip } from "@storyteller/ui-tooltip";
import { defaultModelForPage } from "./defaultModelForPage";
// Model instances are rebuilt when the backend listing hydrates, so compare by
// tauriId rather than object identity.
const isSameModel = (a: Model | undefined, b: Model | undefined): boolean =>
a !== undefined && b !== undefined && a.tauriId === b.tauriId;
interface ClassyModelSelectorProps {
items: Omit<PopoverItem, "selected">[];
page: ModelPage;
@@ -175,7 +181,7 @@ export function ClassyModelSelector({
return {
...item,
selected: item.model === selectedModel,
selected: isSameModel(item.model as Model | undefined, selectedModel),
hoverTooltip: hasMultipleProviders
? (close: () => void) => (
<ProviderTooltipContent
@@ -190,7 +196,7 @@ export function ClassyModelSelector({
: undefined,
tooltipDelayMs: providerTooltipDelayMs,
trailing:
item.model !== selectedModel && hasMultipleProviders
!isSameModel(item.model as Model | undefined, selectedModel) && hasMultipleProviders
? (() => {
const prov = modelId
? selectedProvidersByModel[modelId]
@@ -206,7 +212,7 @@ export function ClassyModelSelector({
})()
: undefined,
selectedRight:
item.model === selectedModel &&
isSameModel(item.model as Model | undefined, selectedModel) &&
selectedProvider &&
hasMultipleProviders ? (
<div className="mr-1 rounded-md p-1.5 bg-primary/60 group-hover:bg-primary/80 transition-colors">
@@ -265,7 +271,7 @@ export function ClassyModelSelector({
{...popoverProps}
buttonClassName="rounded-xl bg-ui-controls/90 hover:bg-ui-controls text-left shadow-sm px-3 py-1 gap-3 border border-ui-controls-border"
renderTrigger={(selectedItem) => {
const modelTitle = selectedItem?.label ?? "";
const modelTitle = selectedItem?.label ?? selectedModel?.selectorName ?? "";
const providerIcon = selectedProvider
? getProviderIcon(selectedProvider)
: null;
+2
View File
@@ -24,3 +24,5 @@ export * from "./lib/classes/Object3DModel.js";
export * from "./lib/lists/Object3DModels.js";
export * from "./lib/loader/modelReconciliation.js";
export * from "./lib/loader/buildModelsFromListing.js";
export * from "./lib/loader/imageModelFeatures.js";
export * from "./lib/loader/videoModelFeatures.js";
@@ -1,594 +1,89 @@
import { GenerationProvider } from "@storyteller/api-enums";
// Static image model overlay, assembled from the per-feature lookup
// functions in `../loader/imageModelFeatures.ts`. At runtime the backend
// omni listing drives membership + capabilities (`buildModelsFromListing.ts`);
// these instances only contribute frontend-only data (presentation, page
// flags, providers) and capability fallbacks.
import { ImageModel } from "../classes/ImageModel.js";
import {
KNOWN_IMAGE_TAURI_IDS,
imageAspectRatios,
imageCanEditAngles,
imageCanEditImages,
imageCanTextToImage,
imageCanUseImagePrompt,
imageCreator,
imageDefaultAspectRatio,
imageDefaultGenerationCount,
imageDefaultQuality,
imageDefaultResolution,
imageEditingIsInpainting,
imageFullName,
imageLegacyId,
imageMaxGenerationCount,
imageMaxImagePromptCount,
imageMaxPromptLength,
imagePredefinedGenerationCounts,
imageProgressBarTime,
imageProviders,
imageQualityOptions,
imageResolutions,
imageSelectorBadges,
imageSelectorDescription,
imageSelectorName,
imageTags,
imageUsesInpaintingMask,
} from "../loader/imageModelFeatures.js";
import { ModelCreator } from "../classes/metadata/ModelCreator.js";
import { ModelTag } from "../classes/metadata/ModelTag.js";
import { CommonAspectRatio } from "../classes/properties/CommonAspectRatio.js";
import { CommonResolution } from "../classes/properties/CommonResolution.js";
import { CommonQuality } from "../classes/properties/CommonQuality.js";
// TODO: Some of the model configs, such as generation counts, are authoritatively controlled in `legacy/Models.ts`
const buildStaticImageModel = (tauriId: string): ImageModel =>
new ImageModel({
// Identity.
id: imageLegacyId(tauriId) ?? tauriId,
tauriId,
fullName: imageFullName(tauriId) ?? tauriId,
category: "image",
creator: imageCreator(tauriId) ?? ModelCreator.ArtCraft,
export const IMAGE_MODELS: ImageModel[] = [
new ImageModel({
id: "grok_image",
tauriId: "grok_imagine_image",
fullName: "Grok",
category: "image",
creator: ModelCreator.Grok,
providers: [GenerationProvider.Grok],
selectorName: "Grok",
selectorDescription: "Fast af",
selectorBadges: ["10 sec."],
// NB: Grok only supports 6 images at a time
defaultGenerationCount: 6,
maxGenerationCount: 6,
predefinedGenerationCounts: [6],
canUseImagePrompt: false,
maxImagePromptCount: 6,
progressBarTime: 10000,
maxPromptLength: 8000,
// Presentation.
selectorName:
imageSelectorName(tauriId) ?? imageFullName(tauriId) ?? tauriId,
selectorDescription: imageSelectorDescription(tauriId) ?? "",
selectorBadges: imageSelectorBadges(tauriId) ?? [],
tags: imageTags(tauriId),
progressBarTime: imageProgressBarTime(tauriId),
// Desktop-native provider knowledge.
providers: imageProviders(tauriId),
// Page-subsetting flags.
canTextToImage: imageCanTextToImage(tauriId),
canEditImages: imageCanEditImages(tauriId),
usesInpaintingMask: imageUsesInpaintingMask(tauriId),
editingIsInpainting: imageEditingIsInpainting(tauriId),
canEditAngles: imageCanEditAngles(tauriId),
// Capability fallbacks (the API overrides these when it knows better).
maxPromptLength: imageMaxPromptLength(tauriId),
maxGenerationCount: imageMaxGenerationCount(tauriId) ?? 4,
defaultGenerationCount: imageDefaultGenerationCount(tauriId) ?? 1,
predefinedGenerationCounts: imagePredefinedGenerationCounts(tauriId),
canUseImagePrompt: imageCanUseImagePrompt(tauriId),
maxImagePromptCount: imageMaxImagePromptCount(tauriId),
// Every current overlay entry enables aspect ratio switching.
canChangeAspectRatio: true,
defaultAspectRatio: CommonAspectRatio.Square,
aspectRatios: [
CommonAspectRatio.Square,
CommonAspectRatio.WideThreeByTwo,
CommonAspectRatio.TallTwoByThree,
],
}),
new ImageModel({
id: "midjourney",
tauriId: "midjourney",
fullName: "Midjourney",
category: "image",
creator: ModelCreator.Midjourney,
providers: [GenerationProvider.Midjourney],
selectorName: "Midjourney",
selectorDescription: "Stunning style and quality",
selectorBadges: ["45 sec."],
// NB: Midjourney only supports 4 images at a time
maxGenerationCount: 4,
defaultGenerationCount: 4,
predefinedGenerationCounts: [4],
canUseImagePrompt: false,
maxImagePromptCount: 6,
progressBarTime: 45000,
maxPromptLength: 6000,
canChangeAspectRatio: true,
// TODO: Rust client doesn't support aspect ratios for MidJourney yet.
}),
new ImageModel({
id: "flux_pro_1_1_ultra",
tauriId: "flux_pro_1p1_ultra",
fullName: "Flux Pro 1.1 Ultra",
category: "image",
creator: ModelCreator.BlackForestLabs,
selectorName: "Flux Pro 1.1 Ultra",
selectorDescription: "Higher quality model",
selectorBadges: ["35 sec."],
maxGenerationCount: 4,
defaultGenerationCount: 4,
canUseImagePrompt: true,
maxImagePromptCount: 6,
progressBarTime: 35000,
maxPromptLength: 4000,
canChangeAspectRatio: true,
defaultAspectRatio: CommonAspectRatio.Square,
aspectRatios: [
CommonAspectRatio.Square,
CommonAspectRatio.WideThreeByTwo,
CommonAspectRatio.WideFourByThree,
CommonAspectRatio.WideSixteenByNine,
CommonAspectRatio.WideTwentyOneByNine,
CommonAspectRatio.TallTwoByThree,
CommonAspectRatio.TallThreeByFour,
CommonAspectRatio.TallNineBySixteen,
CommonAspectRatio.TallNineByTwentyOne,
],
}),
new ImageModel({
id: "flux_pro_1_1",
tauriId: "flux_pro_1p1",
fullName: "Flux Pro 1.1",
category: "image",
creator: ModelCreator.BlackForestLabs,
selectorName: "Flux Pro 1.1",
selectorDescription: "High quality model",
selectorBadges: ["10 sec."],
maxGenerationCount: 4,
defaultGenerationCount: 4,
canUseImagePrompt: true,
maxImagePromptCount: 6,
progressBarTime: 10000,
maxPromptLength: 4000,
canChangeAspectRatio: true,
defaultAspectRatio: CommonAspectRatio.Square,
aspectRatios: [
CommonAspectRatio.Square,
CommonAspectRatio.WideFourByThree,
CommonAspectRatio.WideSixteenByNine,
CommonAspectRatio.TallThreeByFour,
CommonAspectRatio.TallNineBySixteen,
],
}),
new ImageModel({
id: "flux_1_dev",
tauriId: "flux_1_dev",
fullName: "Flux 1 Dev",
category: "image",
creator: ModelCreator.BlackForestLabs,
providers: [
GenerationProvider.Artcraft,
GenerationProvider.Fal,
],
selectorName: "Flux 1 Dev",
selectorDescription: "Fast, but lower quality",
selectorBadges: ["10 sec."],
maxGenerationCount: 4,
defaultGenerationCount: 4,
canUseImagePrompt: true,
maxImagePromptCount: 6,
progressBarTime: 10000,
maxPromptLength: 4000,
canChangeAspectRatio: true,
defaultAspectRatio: CommonAspectRatio.Square,
aspectRatios: [
CommonAspectRatio.Square,
CommonAspectRatio.SquareHd,
CommonAspectRatio.WideFourByThree,
CommonAspectRatio.WideSixteenByNine,
CommonAspectRatio.TallThreeByFour,
CommonAspectRatio.TallNineBySixteen,
],
}),
new ImageModel({
id: "flux_1_schnell",
tauriId: "flux_1_schnell",
fullName: "Flux 1 Schnell",
category: "image",
creator: ModelCreator.BlackForestLabs,
providers: [
GenerationProvider.Artcraft,
GenerationProvider.Fal,
],
selectorName: "Flux 1 Schnell",
selectorDescription: "Fastest image gen, but lowest quality",
selectorBadges: ["10 sec."],
maxGenerationCount: 4,
defaultGenerationCount: 4,
canUseImagePrompt: true,
maxImagePromptCount: 6,
progressBarTime: 10000,
maxPromptLength: 4000,
canChangeAspectRatio: true,
defaultAspectRatio: CommonAspectRatio.Square,
aspectRatios: [
CommonAspectRatio.Square,
CommonAspectRatio.SquareHd,
CommonAspectRatio.WideFourByThree,
CommonAspectRatio.WideSixteenByNine,
CommonAspectRatio.TallThreeByFour,
CommonAspectRatio.TallNineBySixteen,
],
}),
new ImageModel({
id: "nano_banana",
tauriId: "nano_banana",
fullName: "Nano Banana",
category: "image",
creator: ModelCreator.Google,
selectorName: "Nano Banana",
selectorDescription: "Fast instructive editing",
selectorBadges: ["25 sec."],
maxGenerationCount: 4,
defaultGenerationCount: 1,
canEditImages: true,
canUseImagePrompt: true,
maxImagePromptCount: 6,
canTextToImage: true,
tags: [ModelTag.InstructiveEdit],
progressBarTime: 25000,
maxPromptLength: Infinity,
// canChangeResolution: true,
canChangeAspectRatio: true,
defaultAspectRatio: CommonAspectRatio.WideSixteenByNine,
aspectRatios: [
CommonAspectRatio.Auto,
CommonAspectRatio.Square,
CommonAspectRatio.WideFiveByFour,
CommonAspectRatio.WideFourByThree,
CommonAspectRatio.WideThreeByTwo,
CommonAspectRatio.WideSixteenByNine,
CommonAspectRatio.WideTwentyOneByNine,
CommonAspectRatio.TallFourByFive,
CommonAspectRatio.TallThreeByFour,
CommonAspectRatio.TallTwoByThree,
CommonAspectRatio.TallNineBySixteen,
],
}),
new ImageModel({
id: "nano_banana_2",
tauriId: "nano_banana_2",
fullName: "Nano Banana 2",
category: "image",
creator: ModelCreator.Google,
providers: [
GenerationProvider.Artcraft,
GenerationProvider.Fal,
],
selectorName: "Nano Banana 2",
selectorDescription: "Fast instructive editing",
selectorBadges: ["25 sec."],
maxGenerationCount: 4,
defaultGenerationCount: 1,
canEditImages: true,
canUseImagePrompt: true,
maxImagePromptCount: 6,
canTextToImage: true,
tags: [ModelTag.InstructiveEdit],
progressBarTime: 25000,
maxPromptLength: Infinity,
canChangeResolution: true,
canChangeAspectRatio: true,
defaultAspectRatio: CommonAspectRatio.WideSixteenByNine,
aspectRatios: [
CommonAspectRatio.Auto,
CommonAspectRatio.Square,
CommonAspectRatio.WideFiveByFour,
CommonAspectRatio.WideFourByThree,
CommonAspectRatio.WideThreeByTwo,
CommonAspectRatio.WideSixteenByNine,
CommonAspectRatio.WideTwentyOneByNine,
CommonAspectRatio.TallFourByFive,
CommonAspectRatio.TallThreeByFour,
CommonAspectRatio.TallTwoByThree,
CommonAspectRatio.TallNineBySixteen,
],
resolutions: [
CommonResolution.OneK,
CommonResolution.TwoK,
CommonResolution.FourK,
],
}),
new ImageModel({
id: "nano_banana_pro",
tauriId: "nano_banana_pro",
fullName: "Nano Banana Pro",
category: "image",
creator: ModelCreator.Google,
providers: [
GenerationProvider.Artcraft,
GenerationProvider.Fal,
],
selectorName: "Nano Banana Pro",
selectorDescription: "Powerful instructive editing",
selectorBadges: ["30 sec."],
maxGenerationCount: 4,
defaultGenerationCount: 1,
canEditImages: true,
canUseImagePrompt: true,
maxImagePromptCount: 4,
canTextToImage: true,
tags: [ModelTag.InstructiveEdit],
progressBarTime: 25000,
maxPromptLength: Infinity,
canChangeResolution: true,
canChangeAspectRatio: true,
defaultAspectRatio: CommonAspectRatio.WideSixteenByNine,
aspectRatios: [
CommonAspectRatio.Auto,
CommonAspectRatio.Square,
CommonAspectRatio.WideFiveByFour,
CommonAspectRatio.WideFourByThree,
CommonAspectRatio.WideThreeByTwo,
CommonAspectRatio.WideSixteenByNine,
CommonAspectRatio.WideTwentyOneByNine,
CommonAspectRatio.TallFourByFive,
CommonAspectRatio.TallThreeByFour,
CommonAspectRatio.TallTwoByThree,
CommonAspectRatio.TallNineBySixteen,
],
resolutions: [
CommonResolution.OneK,
CommonResolution.TwoK,
CommonResolution.FourK,
],
}),
new ImageModel({
id: "gpt_image_1",
tauriId: "gpt_image_1",
fullName: "GPT Image 1 (GPT-4o)",
category: "image",
creator: ModelCreator.OpenAi,
providers: [GenerationProvider.Artcraft, GenerationProvider.Sora],
selectorName: "GPT Image 1 (GPT-4o)",
selectorDescription: "Slow, but super smart",
selectorBadges: ["60 sec."],
maxGenerationCount: 4,
defaultGenerationCount: 1,
tags: [ModelTag.InstructiveEdit],
canEditImages: true,
canUseImagePrompt: true,
maxImagePromptCount: 6,
progressBarTime: 60000,
maxPromptLength: Infinity,
canChangeAspectRatio: true,
defaultQuality: CommonQuality.High,
qualityOptions: [
CommonQuality.High,
CommonQuality.Medium,
CommonQuality.Low,
],
}),
new ImageModel({
id: "gpt_image_1p5",
tauriId: "gpt_image_1p5",
fullName: "GPT Image 1.5",
category: "image",
creator: ModelCreator.OpenAi,
selectorName: "GPT Image 1.5",
selectorDescription: "Faster, improved",
selectorBadges: ["60 sec."],
maxGenerationCount: 4,
defaultGenerationCount: 1,
tags: [ModelTag.InstructiveEdit],
canEditImages: true,
canUseImagePrompt: true,
maxImagePromptCount: 6,
progressBarTime: 60000,
maxPromptLength: Infinity,
canChangeAspectRatio: true,
defaultAspectRatio: CommonAspectRatio.Square,
aspectRatios: [
CommonAspectRatio.Square,
CommonAspectRatio.Wide,
CommonAspectRatio.Tall,
],
defaultQuality: CommonQuality.High,
qualityOptions: [
CommonQuality.High,
CommonQuality.Medium,
CommonQuality.Low,
],
}),
new ImageModel({
id: "gpt_image_2",
tauriId: "gpt_image_2",
fullName: "GPT Image 2",
category: "image",
creator: ModelCreator.OpenAi,
selectorName: "GPT Image 2",
selectorDescription: "Smart with great text support",
selectorBadges: ["2 min."],
maxGenerationCount: 4,
defaultGenerationCount: 1,
tags: [ModelTag.InstructiveEdit],
canEditImages: true,
canUseImagePrompt: true,
maxImagePromptCount: 6,
progressBarTime: 120000,
maxPromptLength: Infinity,
canChangeAspectRatio: true,
defaultAspectRatio: CommonAspectRatio.WideSixteenByNine,
aspectRatios: [
CommonAspectRatio.Auto,
CommonAspectRatio.Square,
CommonAspectRatio.SquareHd,
CommonAspectRatio.TallThreeByFour,
CommonAspectRatio.TallNineBySixteen,
CommonAspectRatio.WideFourByThree,
CommonAspectRatio.WideSixteenByNine,
],
defaultQuality: CommonQuality.High,
qualityOptions: [
CommonQuality.High,
CommonQuality.Medium,
CommonQuality.Low,
],
}),
new ImageModel({
id: "seedream_4",
tauriId: "seedream_4",
fullName: "Seedream 4",
category: "image",
creator: ModelCreator.Bytedance,
selectorName: "Seedream 4",
selectorDescription: "Fast",
selectorBadges: ["60 sec."],
maxGenerationCount: 4,
defaultGenerationCount: 1,
tags: [ModelTag.InstructiveEdit],
canEditImages: true,
canUseImagePrompt: true,
maxImagePromptCount: 6,
progressBarTime: 60000,
maxPromptLength: 4000,
canChangeAspectRatio: true,
defaultAspectRatio: CommonAspectRatio.WideSixteenByNine,
aspectRatios: [
CommonAspectRatio.Auto2k,
CommonAspectRatio.Auto4k,
CommonAspectRatio.Square,
CommonAspectRatio.SquareHd,
CommonAspectRatio.WideFourByThree,
CommonAspectRatio.WideSixteenByNine,
CommonAspectRatio.TallThreeByFour,
CommonAspectRatio.TallNineBySixteen,
],
}),
new ImageModel({
id: "seedream_4p5",
tauriId: "seedream_4p5",
fullName: "Seedream 4.5",
category: "image",
creator: ModelCreator.Bytedance,
selectorName: "Seedream 4.5",
selectorDescription: "Fast",
selectorBadges: ["60 sec."],
maxGenerationCount: 4,
defaultGenerationCount: 1,
tags: [ModelTag.InstructiveEdit],
canEditImages: true,
canUseImagePrompt: true,
maxImagePromptCount: 6,
progressBarTime: 60000,
maxPromptLength: 4000,
canChangeAspectRatio: true,
defaultAspectRatio: CommonAspectRatio.WideSixteenByNine,
aspectRatios: [
CommonAspectRatio.Auto2k,
CommonAspectRatio.Auto4k,
CommonAspectRatio.Square,
CommonAspectRatio.SquareHd,
CommonAspectRatio.WideFourByThree,
CommonAspectRatio.WideSixteenByNine,
CommonAspectRatio.TallThreeByFour,
CommonAspectRatio.TallNineBySixteen,
],
}),
new ImageModel({
id: "seedream_5_lite",
tauriId: "seedream_5_lite",
fullName: "Seedream 5 Lite",
category: "image",
creator: ModelCreator.Bytedance,
selectorName: "Seedream 5 Lite",
selectorDescription: "Fast",
selectorBadges: ["60 sec."],
maxGenerationCount: 4,
defaultGenerationCount: 1,
tags: [ModelTag.InstructiveEdit],
canEditImages: true,
canUseImagePrompt: true,
maxImagePromptCount: 6,
progressBarTime: 60000,
maxPromptLength: 4000,
canChangeAspectRatio: true,
defaultAspectRatio: CommonAspectRatio.WideSixteenByNine,
aspectRatios: [
CommonAspectRatio.Auto2k,
CommonAspectRatio.Square,
CommonAspectRatio.SquareHd,
CommonAspectRatio.WideFourByThree,
CommonAspectRatio.WideSixteenByNine,
CommonAspectRatio.TallThreeByFour,
CommonAspectRatio.TallNineBySixteen,
],
}),
new ImageModel({
id: "qwen_edit_2511_angles",
tauriId: "qwen_edit_2511_angles",
fullName: "Qwen Edit 2511 Angles",
category: "image",
creator: ModelCreator.Alibaba,
selectorName: "Qwen Edit 2511 Angles",
selectorDescription: "Angle manipulation with optional prompt",
selectorBadges: ["30 sec."],
maxGenerationCount: 4,
defaultGenerationCount: 1,
maxPromptLength: 800,
canTextToImage: false,
canEditImages: false,
canEditAngles: true,
canUseImagePrompt: true,
maxImagePromptCount: 1,
progressBarTime: 30000,
canChangeAspectRatio: true,
defaultAspectRatio: CommonAspectRatio.SquareHd,
aspectRatios: [
CommonAspectRatio.Square,
CommonAspectRatio.SquareHd,
CommonAspectRatio.WideFourByThree,
CommonAspectRatio.WideSixteenByNine,
CommonAspectRatio.TallThreeByFour,
CommonAspectRatio.TallNineBySixteen,
],
}),
new ImageModel({
id: "flux_2_lora_angles",
tauriId: "flux_2_lora_angles",
fullName: "Flux 2 LoRA Angles",
category: "image",
creator: ModelCreator.BlackForestLabs,
selectorName: "Flux 2 LoRA Angles",
selectorDescription: "Angle manipulation",
selectorBadges: ["30 sec."],
maxGenerationCount: 4,
defaultGenerationCount: 1,
maxPromptLength: 4000,
canTextToImage: false,
canEditImages: false,
canEditAngles: true,
canUseImagePrompt: true,
maxImagePromptCount: 1,
progressBarTime: 30000,
canChangeAspectRatio: true,
defaultAspectRatio: CommonAspectRatio.SquareHd,
aspectRatios: [
CommonAspectRatio.Square,
CommonAspectRatio.SquareHd,
CommonAspectRatio.WideFourByThree,
CommonAspectRatio.WideSixteenByNine,
CommonAspectRatio.TallThreeByFour,
CommonAspectRatio.TallNineBySixteen,
],
}),
new ImageModel({
id: "flux_pro_kontext_max",
tauriId: "flux_pro_kontext_max",
fullName: "Flux Pro Kontext Max",
category: "image",
creator: ModelCreator.BlackForestLabs,
selectorName: "Flux Pro Kontext Max",
selectorDescription: "Fast instructive editing",
selectorBadges: ["20 sec."],
maxGenerationCount: 4,
defaultGenerationCount: 4,
maxPromptLength: 4000,
canEditImages: true,
canTextToImage: false,
tags: [ModelTag.InstructiveEdit],
progressBarTime: 20000,
canChangeAspectRatio: true,
}),
new ImageModel({
id: "flux_pro_inpaint",
tauriId: "flux_pro_1",
fullName: "Flux Pro Inpaint",
category: "image",
creator: ModelCreator.BlackForestLabs,
selectorName: "Flux Pro (Inpainting)",
selectorDescription: "Fast inpainting",
selectorBadges: ["30 sec."],
maxGenerationCount: 1, // NB: Fal only allows one image for some reason!
maxPromptLength: 4000,
defaultGenerationCount: 1, // NB: Fal only allows one image for some reason!
canEditImages: true,
usesInpaintingMask: true,
editingIsInpainting: true,
canTextToImage: false,
progressBarTime: 30000,
canChangeAspectRatio: true,
}),
new ImageModel({
id: "flux_dev_juggernaut_inpaint",
tauriId: "flux_dev_juggernaut",
fullName: "Flux Dev Juggernaut Inpaint",
category: "image",
creator: ModelCreator.BlackForestLabs,
selectorName: "Flux Dev Juggernaut",
selectorDescription: "Fast inpainting, low quality",
selectorBadges: ["10 sec."],
maxGenerationCount: 4,
defaultGenerationCount: 4,
maxPromptLength: 4000,
canEditImages: true,
usesInpaintingMask: true,
editingIsInpainting: true,
canTextToImage: false,
progressBarTime: 10000,
canChangeAspectRatio: true,
}),
];
aspectRatios: imageAspectRatios(tauriId),
defaultAspectRatio: imageDefaultAspectRatio(tauriId),
canChangeResolution: imageResolutions(tauriId) !== undefined,
resolutions: imageResolutions(tauriId),
defaultResolution: imageDefaultResolution(tauriId),
qualityOptions: imageQualityOptions(tauriId),
defaultQuality: imageDefaultQuality(tauriId),
});
export const IMAGE_MODELS: ImageModel[] = KNOWN_IMAGE_TAURI_IDS.map(
buildStaticImageModel,
);
export const IMAGE_MODELS_BY_ID: Map<string, ImageModel> = new Map(
IMAGE_MODELS.map((model) => [model.id, model]),
@@ -1,616 +1,96 @@
import { GenerationProvider } from "@storyteller/api-enums";
import { VideoModel } from "../classes/VideoModel.js";
import { ModelCreator } from "../classes/metadata/ModelCreator.js";
import { SizeIconOption } from "../classes/metadata/SizeOption.js";
// Static video model overlay, assembled from the per-feature lookup
// functions in `../loader/videoModelFeatures.ts`. At runtime the backend
// omni listing drives membership + capabilities (`buildModelsFromListing.ts`);
// these instances only contribute frontend-only data (presentation, page
// flags, providers) and capability fallbacks.
export const VIDEO_MODELS: VideoModel[] = [
import { VideoModel } from "../classes/VideoModel.js";
import {
KNOWN_VIDEO_TAURI_IDS,
videoCreator,
videoDefaultDuration,
videoDefaultResolution,
videoDurationOptions,
videoEndFrame,
videoFullName,
videoGenerateWithSound,
videoLegacyId,
videoMaxAudioRefDuration,
videoMaxPromptLength,
videoMaxReferenceAudios,
videoMaxReferenceImages,
videoMaxReferenceVideos,
videoMaxVideoRefDuration,
videoProgressBarTime,
videoProviders,
videoRequiresImage,
videoResolutionOptions,
videoSelectorBadges,
videoSelectorDescription,
videoSelectorName,
videoSizeOptions,
videoStartFrame,
videoSupportsCommonAspectRatio,
videoSupportsReferenceMode,
videoSupportsSystemPrompt,
videoTags,
videoTextToVideoSupported,
} from "../loader/videoModelFeatures.js";
import { ModelCreator } from "../classes/metadata/ModelCreator.js";
const buildStaticVideoModel = (tauriId: string): VideoModel =>
new VideoModel({
id: "grok_video",
tauriId: "grok_imagine_video",
fullName: "Grok Video",
// Identity.
id: videoLegacyId(tauriId) ?? tauriId,
tauriId,
fullName: videoFullName(tauriId) ?? tauriId,
category: "video",
creator: ModelCreator.Grok,
providers: [GenerationProvider.Grok],
selectorName: "Grok Video",
selectorDescription: "Fastest video model",
selectorBadges: ["20 sec."],
startFrame: true,
endFrame: false,
requiresImage: true,
progressBarTime: 50000,
maxPromptLength: 4096,
sizeOptions: [
{
tauriValue: "landscape",
textLabel: "Landscape",
icon: SizeIconOption.Landscape,
},
{
tauriValue: "portrait",
textLabel: "Portrait",
icon: SizeIconOption.Portrait,
},
{
tauriValue: "square",
textLabel: "Square",
icon: SizeIconOption.Square,
},
],
}),
new VideoModel({
id: "grok_imagine_video_1p5",
tauriId: "grok_imagine_video_1p5",
fullName: "Grok Imagine 1.5",
category: "video",
creator: ModelCreator.Grok,
// Served via Artcraft (storyteller-web / artcraft_router) for now, even
// though the model is made by xAI. See generate_video_command.rs.
providers: [GenerationProvider.Artcraft],
selectorName: "Grok Imagine 1.5",
selectorDescription: "Image-to-video by xAI",
selectorBadges: ["Preview"],
startFrame: true,
endFrame: false,
requiresImage: true,
textToVideoSupported: false,
progressBarTime: 50000,
maxPromptLength: 4096,
resolutionOptions: ["480p", "720p"],
defaultResolution: "720p",
sizeOptions: [
{
tauriValue: "landscape",
textLabel: "Landscape",
icon: SizeIconOption.Landscape,
},
{
tauriValue: "portrait",
textLabel: "Portrait",
icon: SizeIconOption.Portrait,
},
{
tauriValue: "square",
textLabel: "Square",
icon: SizeIconOption.Square,
},
],
}),
new VideoModel({
id: "kling_1_6_pro",
tauriId: "kling_1p6_pro",
fullName: "Kling 1.6 Pro",
category: "video",
creator: ModelCreator.Kling,
selectorName: "Kling 1.6 Pro",
selectorDescription: "Fast video model",
selectorBadges: ["2 min."],
startFrame: true,
endFrame: true,
requiresImage: true,
progressBarTime: 300000,
maxPromptLength: 2500,
generateWithSound: false,
}),
new VideoModel({
id: "kling_2_1_pro",
tauriId: "kling_2p1_pro",
fullName: "Kling 2.1 Pro",
category: "video",
creator: ModelCreator.Kling,
selectorName: "Kling 2.1 Pro",
selectorDescription: "Fast video model",
selectorBadges: ["2 min."],
startFrame: true,
endFrame: true,
requiresImage: true,
progressBarTime: 300000,
maxPromptLength: 2500,
generateWithSound: false,
}),
new VideoModel({
id: "kling_2_1_master",
tauriId: "kling_2p1_master",
fullName: "Kling 2.1 Master",
category: "video",
creator: ModelCreator.Kling,
selectorName: "Kling 2.1 Master",
selectorDescription: "Fast video model",
selectorBadges: ["2 min."],
startFrame: true,
endFrame: false,
requiresImage: true,
progressBarTime: 300000,
maxPromptLength: 2500,
generateWithSound: false,
}),
new VideoModel({
id: "kling_2p5_turbo_pro",
tauriId: "kling_2p5_turbo_pro",
fullName: "Kling 2.5 Turbo Pro",
category: "video",
creator: ModelCreator.Kling,
selectorName: "Kling 2.5 Turbo Pro",
selectorDescription: "Fast video model",
selectorBadges: ["2 min."],
startFrame: true,
endFrame: true,
requiresImage: false,
progressBarTime: 300000,
maxPromptLength: 2500,
generateWithSound: false,
}),
new VideoModel({
id: "kling_2p6_pro",
tauriId: "kling_2p6_pro",
fullName: "Kling 2.6 Pro",
category: "video",
creator: ModelCreator.Kling,
selectorName: "Kling 2.6 Pro",
selectorDescription: "Fast video model",
selectorBadges: ["2 min."],
startFrame: true,
endFrame: false,
requiresImage: true,
progressBarTime: 300000,
maxPromptLength: 2500,
generateWithSound: false,
}),
new VideoModel({
id: "kling_3p0_standard",
tauriId: "kling_3p0_standard",
fullName: "Kling 3.0 Standard",
category: "video",
creator: ModelCreator.Kling,
selectorName: "Kling 3.0 Standard",
selectorDescription: "Standard quality video model",
selectorBadges: ["2 min."],
startFrame: true,
endFrame: true,
requiresImage: false,
sizeOptions: [
{
tauriValue: "wide_sixteen_by_nine",
textLabel: "16:9",
icon: SizeIconOption.Landscape16x9,
},
{
tauriValue: "square",
textLabel: "1:1",
icon: SizeIconOption.Square,
},
{
tauriValue: "tall_nine_by_sixteen",
textLabel: "9:16",
icon: SizeIconOption.Portrait9x16,
},
],
supportsCommonAspectRatio: true,
durationOptions: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
defaultDuration: 5,
progressBarTime: 300000,
maxPromptLength: 2500,
generateWithSound: true,
}),
new VideoModel({
id: "kling_3p0_pro",
tauriId: "kling_3p0_pro",
fullName: "Kling 3.0 Pro",
category: "video",
creator: ModelCreator.Kling,
selectorName: "Kling 3.0 Pro",
selectorDescription: "High quality video model",
selectorBadges: ["2 min."],
startFrame: true,
endFrame: true,
requiresImage: false,
sizeOptions: [
{
tauriValue: "wide_sixteen_by_nine",
textLabel: "16:9",
icon: SizeIconOption.Landscape16x9,
},
{
tauriValue: "square",
textLabel: "1:1",
icon: SizeIconOption.Square,
},
{
tauriValue: "tall_nine_by_sixteen",
textLabel: "9:16",
icon: SizeIconOption.Portrait9x16,
},
],
supportsCommonAspectRatio: true,
durationOptions: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
defaultDuration: 5,
progressBarTime: 300000,
maxPromptLength: 2500,
generateWithSound: true,
}),
new VideoModel({
id: "happy_horse_1p0",
tauriId: "happy_horse_1p0",
fullName: "Happy Horse 1.0",
category: "video",
creator: ModelCreator.Alibaba,
selectorName: "Happy Horse 1.0",
selectorDescription: "High quality video model",
selectorBadges: ["2 min."],
startFrame: true,
endFrame: false,
requiresImage: false,
resolutionOptions: ["720p", "1080p"],
sizeOptions: [
{
tauriValue: "wide_sixteen_by_nine",
textLabel: "16:9",
icon: SizeIconOption.Landscape16x9,
},
{
tauriValue: "wide_four_by_three",
textLabel: "4:3",
icon: SizeIconOption.Standard4x3,
},
{
tauriValue: "square",
textLabel: "1:1",
icon: SizeIconOption.Square,
},
{
tauriValue: "tall_three_by_four",
textLabel: "3:4",
icon: SizeIconOption.Portrait3x4,
},
{
tauriValue: "tall_nine_by_sixteen",
textLabel: "9:16",
icon: SizeIconOption.Portrait9x16,
},
],
supportsCommonAspectRatio: true,
durationOptions: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
defaultDuration: 5,
progressBarTime: 300000,
maxPromptLength: 2500,
generateWithSound: true,
}),
new VideoModel({
id: "seedance_1_0_lite",
tauriId: "seedance_1p0_lite",
fullName: "Seedance 1.0 Lite",
category: "video",
creator: ModelCreator.Bytedance,
selectorName: "Seedance 1.0 Lite",
selectorDescription: "Fast video model",
selectorBadges: ["2 min."],
startFrame: true,
endFrame: false,
requiresImage: true,
progressBarTime: 100000,
maxPromptLength: 3000,
}),
new VideoModel({
id: "seedance_1p5_pro",
tauriId: "seedance_1p5_pro",
fullName: "Seedance 1.5 Pro",
category: "video",
creator: ModelCreator.Bytedance,
selectorName: "Seedance 1.5 Pro",
selectorDescription: "High quality video model",
selectorBadges: ["2 min."],
startFrame: true,
endFrame: true,
requiresImage: false,
sizeOptions: [
{
tauriValue: "wide_sixteen_by_nine",
textLabel: "16:9",
icon: SizeIconOption.Landscape16x9,
},
{
tauriValue: "square",
textLabel: "1:1",
icon: SizeIconOption.Square,
},
{
tauriValue: "tall_three_by_four",
textLabel: "3:4",
icon: SizeIconOption.Portrait3x4,
},
{
tauriValue: "wide_four_by_three",
textLabel: "4:3",
icon: SizeIconOption.Standard4x3,
},
{
tauriValue: "tall_nine_by_sixteen",
textLabel: "9:16",
icon: SizeIconOption.Portrait9x16,
},
{
tauriValue: "wide_twenty_one_by_nine",
textLabel: "21:9",
icon: SizeIconOption.Landscape,
},
],
durationOptions: [4, 5, 6, 7, 8, 9, 10, 11, 12],
defaultDuration: 5,
resolutionOptions: ["480p", "720p", "1080p"],
defaultResolution: "720p",
supportsSystemPrompt: false,
generateWithSound: true,
progressBarTime: 180000,
maxPromptLength: 3000,
}),
new VideoModel({
id: "seedance_2p0",
tauriId: "seedance_2p0",
fullName: "Seedance 2.0",
category: "video",
creator: ModelCreator.Bytedance,
selectorName: "Seedance 2.0",
selectorDescription: "High quality model",
selectorBadges: ["~15 min."],
startFrame: true,
endFrame: true,
requiresImage: false,
sizeOptions: [
{
tauriValue: "wide_twenty_one_by_nine",
textLabel: "21:9",
icon: SizeIconOption.Landscape16x9,
},
{
tauriValue: "wide_sixteen_by_nine",
textLabel: "16:9",
icon: SizeIconOption.Landscape16x9,
},
{
tauriValue: "square",
textLabel: "1:1",
icon: SizeIconOption.Square,
},
{
tauriValue: "tall_three_by_four",
textLabel: "3:4",
icon: SizeIconOption.Portrait3x4,
},
{
tauriValue: "wide_four_by_three",
textLabel: "4:3",
icon: SizeIconOption.Standard4x3,
},
{
tauriValue: "tall_nine_by_sixteen",
textLabel: "9:16",
icon: SizeIconOption.Portrait9x16,
},
],
supportsCommonAspectRatio: true,
durationOptions: [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
defaultDuration: 5,
supportsReferenceMode: true,
maxReferenceImages: 9,
maxReferenceVideos: 3,
maxVideoRefDuration: 15,
maxReferenceAudios: 3,
maxAudioRefDuration: 15,
resolutionOptions: ["480p", "720p", "1080p"],
defaultResolution: "720p",
supportsSystemPrompt: false,
progressBarTime: 900000, // ~15 minutes
maxPromptLength: Infinity,
}),
new VideoModel({
id: "seedance_2p0_fast",
tauriId: "seedance_2p0_fast",
fullName: "Seedance 2.0 Fast",
category: "video",
creator: ModelCreator.Bytedance,
selectorName: "Seedance 2.0 Fast",
selectorDescription: "Fast and high quality",
selectorBadges: ["~5 min."],
startFrame: true,
endFrame: true,
requiresImage: false,
sizeOptions: [
{
tauriValue: "wide_twenty_one_by_nine",
textLabel: "21:9",
icon: SizeIconOption.Landscape16x9,
},
{
tauriValue: "wide_sixteen_by_nine",
textLabel: "16:9",
icon: SizeIconOption.Landscape16x9,
},
{
tauriValue: "square",
textLabel: "1:1",
icon: SizeIconOption.Square,
},
{
tauriValue: "tall_three_by_four",
textLabel: "3:4",
icon: SizeIconOption.Portrait3x4,
},
{
tauriValue: "wide_four_by_three",
textLabel: "4:3",
icon: SizeIconOption.Standard4x3,
},
{
tauriValue: "tall_nine_by_sixteen",
textLabel: "9:16",
icon: SizeIconOption.Portrait9x16,
},
],
supportsCommonAspectRatio: true,
durationOptions: [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
defaultDuration: 5,
supportsReferenceMode: true,
maxReferenceImages: 9,
maxReferenceVideos: 3,
maxVideoRefDuration: 15,
maxReferenceAudios: 3,
maxAudioRefDuration: 15,
resolutionOptions: ["480p", "720p"],
defaultResolution: "720p",
supportsSystemPrompt: false,
progressBarTime: 300000, // ~5 minutes
maxPromptLength: Infinity,
}),
new VideoModel({
id: "sora_2",
tauriId: "sora_2",
fullName: "Sora 2",
category: "video",
creator: ModelCreator.OpenAi,
providers: [GenerationProvider.Artcraft, GenerationProvider.Sora],
selectorName: "Sora 2",
selectorDescription: "Smart video model",
selectorBadges: ["2 min."],
startFrame: false,
endFrame: false,
requiresImage: false,
sizeOptions: [
{
tauriValue: "landscape",
textLabel: "Landscape",
icon: SizeIconOption.Landscape,
},
{
tauriValue: "portrait",
textLabel: "Portrait",
icon: SizeIconOption.Portrait,
},
],
progressBarTime: 100000,
maxPromptLength: 2000,
}),
new VideoModel({
id: "sora_2_pro",
tauriId: "sora_2_pro",
fullName: "Sora 2 Pro",
category: "video",
creator: ModelCreator.OpenAi,
selectorName: "Sora 2 Pro",
selectorDescription: "Smart video model",
selectorBadges: ["2 min."],
startFrame: false,
endFrame: false,
requiresImage: false,
sizeOptions: [
{
tauriValue: "landscape",
textLabel: "Landscape",
icon: SizeIconOption.Landscape,
},
{
tauriValue: "portrait",
textLabel: "Portrait",
icon: SizeIconOption.Portrait,
},
],
progressBarTime: 100000,
maxPromptLength: 2000,
}),
new VideoModel({
id: "veo_2",
tauriId: "veo_2",
fullName: "Google Veo 2",
category: "video",
creator: ModelCreator.Google,
selectorName: "Google Veo 2",
selectorDescription: "Fast video model",
selectorBadges: ["2 min."],
startFrame: true,
endFrame: false,
requiresImage: true,
generateWithSound: false,
progressBarTime: 100000,
maxPromptLength: 5000,
}),
new VideoModel({
id: "veo_3",
tauriId: "veo_3",
fullName: "Google Veo 3",
category: "video",
creator: ModelCreator.Google,
selectorName: "Google Veo 3",
selectorDescription: "Slow, high-quality model",
selectorBadges: ["2 min."],
startFrame: true,
endFrame: false,
requiresImage: true,
generateWithSound: true,
progressBarTime: 100000,
maxPromptLength: 5000,
}),
new VideoModel({
id: "veo_3_fast",
tauriId: "veo_3_fast",
fullName: "Google Veo 3 Fast",
category: "video",
creator: ModelCreator.Google,
selectorName: "Google Veo 3 Fast",
selectorDescription: "High-quality model (faster)",
selectorBadges: ["2 min."],
startFrame: true,
endFrame: false,
requiresImage: true,
generateWithSound: true,
progressBarTime: 100000,
maxPromptLength: 5000,
}),
new VideoModel({
id: "veo_3p1",
tauriId: "veo_3p1",
fullName: "Google Veo 3.1",
category: "video",
creator: ModelCreator.Google,
selectorName: "Google Veo 3.1",
selectorDescription: "Slow, high-quality model",
selectorBadges: ["2 min."],
startFrame: true,
endFrame: false,
requiresImage: true,
generateWithSound: true,
progressBarTime: 100000,
maxPromptLength: 5000,
}),
new VideoModel({
id: "veo_3p1_fast",
tauriId: "veo_3p1_fast",
fullName: "Google Veo 3.1 Fast",
category: "video",
creator: ModelCreator.Google,
selectorName: "Google Veo 3.1 Fast",
selectorDescription: "High-quality model (faster)",
selectorBadges: ["2 min."],
startFrame: true,
endFrame: false,
requiresImage: true,
generateWithSound: true,
progressBarTime: 100000,
maxPromptLength: 5000,
}),
new VideoModel({
id: "switch_x",
tauriId: "switch_x",
fullName: "Beeble SwitchX",
category: "video",
creator: ModelCreator.Beeble,
selectorName: "Beeble SwitchX",
selectorDescription: "Relight, change location, swap objects.",
selectorBadges: ["5 min."],
startFrame: false,
endFrame: false,
requiresImage: true,
progressBarTime: 5 * 60 * 1000,
maxPromptLength: 2000,
}),
];
creator: videoCreator(tauriId) ?? ModelCreator.ArtCraft,
// Presentation.
selectorName:
videoSelectorName(tauriId) ?? videoFullName(tauriId) ?? tauriId,
selectorDescription: videoSelectorDescription(tauriId) ?? "",
selectorBadges: videoSelectorBadges(tauriId) ?? [],
tags: videoTags(tauriId),
progressBarTime: videoProgressBarTime(tauriId),
// Desktop-native provider knowledge.
providers: videoProviders(tauriId),
// Prompting.
maxPromptLength: videoMaxPromptLength(tauriId),
supportsSystemPrompt: videoSupportsSystemPrompt(tauriId),
textToVideoSupported: videoTextToVideoSupported(tauriId),
generateWithSound: videoGenerateWithSound(tauriId),
// Keyframes / input images.
startFrame: videoStartFrame(tauriId) ?? false,
endFrame: videoEndFrame(tauriId) ?? false,
requiresImage: videoRequiresImage(tauriId) ?? false,
// Size / aspect ratio.
sizeOptions: videoSizeOptions(tauriId),
supportsCommonAspectRatio: videoSupportsCommonAspectRatio(tauriId),
// Duration / resolution.
durationOptions: videoDurationOptions(tauriId),
defaultDuration: videoDefaultDuration(tauriId),
resolutionOptions: videoResolutionOptions(tauriId),
defaultResolution: videoDefaultResolution(tauriId),
// Reference mode.
supportsReferenceMode: videoSupportsReferenceMode(tauriId),
maxReferenceImages: videoMaxReferenceImages(tauriId),
maxReferenceVideos: videoMaxReferenceVideos(tauriId),
maxVideoRefDuration: videoMaxVideoRefDuration(tauriId),
maxReferenceAudios: videoMaxReferenceAudios(tauriId),
maxAudioRefDuration: videoMaxAudioRefDuration(tauriId),
});
export const VIDEO_MODELS: VideoModel[] = KNOWN_VIDEO_TAURI_IDS.map(
buildStaticVideoModel,
);
export const VIDEO_MODELS_BY_ID: Map<string, VideoModel> = new Map(
VIDEO_MODELS.map((model) => [model.id, model]),
@@ -22,6 +22,8 @@ import { SizeIconOption, SizeOption } from "../classes/metadata/SizeOption.js";
import { CommonAspectRatio } from "../classes/properties/CommonAspectRatio.js";
import { CommonResolution } from "../classes/properties/CommonResolution.js";
import { CommonQuality } from "../classes/properties/CommonQuality.js";
import { imageSelectorName } from "./imageModelFeatures.js";
import { videoSelectorName } from "./videoModelFeatures.js";
import {
BACKEND_TO_TAURI_IMAGE_ID,
BACKEND_TO_TAURI_VIDEO_ID,
@@ -76,43 +78,84 @@ export interface ListingVideoModel extends ListingModelBase {
export const buildImageModelsFromListing = (
overlay: ImageModel[],
listing: ListingImageModel[],
offeredModelIds: string[] = [],
): ImageModel[] =>
build(overlay, listing, BACKEND_TO_TAURI_IMAGE_ID, mergedImageModel);
build(overlay, listing, offeredModelIds, BACKEND_TO_TAURI_IMAGE_ID, mergedImageModel);
export const buildVideoModelsFromListing = (
overlay: VideoModel[],
listing: ListingVideoModel[],
offeredModelIds: string[] = [],
): VideoModel[] =>
build(overlay, listing, BACKEND_TO_TAURI_VIDEO_ID, mergedVideoModel);
build(overlay, listing, offeredModelIds, BACKEND_TO_TAURI_VIDEO_ID, mergedVideoModel);
// ── Core assembly ──────────────────────────────────────────────────────────
// Membership: a model appears in the picker when it is
// 1. OFFERED by a provider in the response's `providers[]` (the backend's
// publish switch — `models[]` details also contain disabled entries and
// internal variants that must NOT surface), or
// 2. an ENABLED `models[]` detail that the frontend knows (overlay entry) —
// covers desktop-native models like grok video that no backend provider
// offers yet, or
// 3. a frontend-only overlay model the backend has never heard of
// (switch_x, inpaint models, …).
// `models[]` details are the capability source either way (the enabled entry
// wins when a model is detailed twice).
const build = <T extends { tauriId: string }, L extends ListingModelBase>(
overlay: T[],
listing: L[],
offeredModelIds: string[],
alias: Record<string, string>,
merge: (m: L, tauriId: string, overlayEntry: T | undefined) => T,
): T[] => {
const toTauriId = (backendId: string) => alias[backendId] ?? backendId;
const overlayByTauriId = new Map(overlay.map((m) => [m.tauriId, m]));
const knownTauriIds = new Set(listing.map((m) => alias[m.model] ?? m.model));
const knownTauriIds = new Set(listing.map((m) => toTauriId(m.model)));
const offeredTauriIds = new Set(offeredModelIds.map(toTauriId));
// Capability lookup — prefer the enabled detail entry when duplicated.
const detailsByTauriId = new Map<string, L>();
for (const m of listing) {
const tauriId = toTauriId(m.model);
const existing = detailsByTauriId.get(tauriId);
if (existing === undefined || existing.is_disabled === true) {
detailsByTauriId.set(tauriId, m);
}
}
const result: T[] = [];
const seenTauriIds = new Set<string>();
// Backend drives membership + order; every entry is rebuilt with API-first
// capabilities.
// Backend details order drives ordering.
for (const m of listing) {
if (m.is_disabled === true) continue;
const tauriId = alias[m.model] ?? m.model;
// Guard: several backend ids aliasing to one frontend model would render
// duplicates — only surface the first.
const tauriId = toTauriId(m.model);
if (seenTauriIds.has(tauriId)) continue;
const overlayEntry = overlayByTauriId.get(tauriId);
const detail = detailsByTauriId.get(tauriId) ?? m;
const enabled = detail.is_disabled !== true;
const show =
offeredTauriIds.has(tauriId) || (enabled && overlayEntry !== undefined);
if (!show) continue;
seenTauriIds.add(tauriId);
result.push(merge(detail, tauriId, overlayEntry));
}
// Offered models with no detail entry at all (unusual, but the publish
// switch wins): surface minimally.
for (const backendId of offeredModelIds) {
const tauriId = toTauriId(backendId);
if (seenTauriIds.has(tauriId)) continue;
seenTauriIds.add(tauriId);
result.push(merge(m, tauriId, overlayByTauriId.get(tauriId)));
result.push(
merge({ model: backendId } as L, tauriId, overlayByTauriId.get(tauriId)),
);
}
// Append frontend-only models the backend has never heard of (switch_x,
// inpaint models, …).
// Append frontend-only models the backend has never heard of.
for (const m of overlay) {
if (!knownTauriIds.has(m.tauriId)) result.push(m);
if (!knownTauriIds.has(m.tauriId) && !seenTauriIds.has(m.tauriId)) {
result.push(m);
}
}
return result;
};
@@ -138,7 +181,7 @@ const mergedImageModel = (
creator: creatorFor(m.model_creator, m.model, o?.creator),
// Presentation — the omni image API has no display fields yet.
selectorName: o?.selectorName ?? fullName,
selectorName: imageSelectorName(tauriId) ?? fullName,
selectorDescription: o?.selectorDescription ?? "",
selectorBadges: o?.selectorBadges ?? [],
tags: o?.tags ?? [],
@@ -205,7 +248,7 @@ const mergedVideoModel = (
// Presentation. The video API DOES have descriptions (extra_info_short /
// extra_info) — prefer them so backend copy reaches the picker.
selectorName: o?.selectorName ?? fullName,
selectorName: videoSelectorName(tauriId) ?? fullName,
selectorDescription:
m.extra_info_short ?? m.extra_info ?? o?.selectorDescription ?? "",
selectorBadges: o?.selectorBadges ?? [],
@@ -0,0 +1,476 @@
// Frontend-only per-feature data for image models, keyed by `tauriId`.
//
// Each exported function is a sparse lookup: a model has an entry ONLY when
// its value differs from the class default (see `ImageModel` / `Model`), so
// every feature is individually inspectable and deletable once the backend
// omni listing serves it. `buildModelsFromListing.ts` merges the API
// capability fields over these values at runtime.
import { GenerationProvider } from "@storyteller/api-enums";
import { ModelCreator } from "../classes/metadata/ModelCreator.js";
import { ModelTag } from "../classes/metadata/ModelTag.js";
import { CommonAspectRatio } from "../classes/properties/CommonAspectRatio.js";
import { CommonResolution } from "../classes/properties/CommonResolution.js";
import { CommonQuality } from "../classes/properties/CommonQuality.js";
// All image model tauriIds, in picker (list) order.
export const KNOWN_IMAGE_TAURI_IDS: string[] = [
"grok_imagine_image",
"midjourney",
"flux_pro_1p1_ultra",
"flux_pro_1p1",
"flux_1_dev",
"flux_1_schnell",
"nano_banana",
"nano_banana_2",
"nano_banana_pro",
"gpt_image_1",
"gpt_image_1p5",
"gpt_image_2",
"seedream_4",
"seedream_4p5",
"seedream_5_lite",
"qwen_edit_2511_angles",
"flux_2_lora_angles",
"flux_pro_kontext_max",
"flux_pro_1",
"flux_dev_juggernaut",
];
// Models the backend omni listing does not know about; the loader appends
// these after the backend-driven entries.
export const FRONTEND_ONLY_IMAGE_TAURI_IDS: string[] = [
"midjourney",
"grok_imagine_image",
"qwen_edit_2511_angles",
"flux_2_lora_angles",
"flux_pro_kontext_max",
"flux_pro_1",
"flux_dev_juggernaut",
];
// ── Identity ────────────────────────────────────────────────────────────────
// Frontend `id` where it historically diverged from `tauriId` (BY_ID lookups
// and generation history depend on these).
export const imageLegacyId = (tauriId: string): string | undefined => {
const values: Record<string, string> = {
grok_imagine_image: "grok_image",
flux_pro_1p1_ultra: "flux_pro_1_1_ultra",
flux_pro_1p1: "flux_pro_1_1",
flux_pro_1: "flux_pro_inpaint",
flux_dev_juggernaut: "flux_dev_juggernaut_inpaint",
};
return values[tauriId];
};
export const imageCreator = (tauriId: string): ModelCreator | undefined => {
const values: Record<string, ModelCreator> = {
grok_imagine_image: ModelCreator.Grok,
midjourney: ModelCreator.Midjourney,
flux_pro_1p1_ultra: ModelCreator.BlackForestLabs,
flux_pro_1p1: ModelCreator.BlackForestLabs,
flux_1_dev: ModelCreator.BlackForestLabs,
flux_1_schnell: ModelCreator.BlackForestLabs,
nano_banana: ModelCreator.Google,
nano_banana_2: ModelCreator.Google,
nano_banana_pro: ModelCreator.Google,
gpt_image_1: ModelCreator.OpenAi,
gpt_image_1p5: ModelCreator.OpenAi,
gpt_image_2: ModelCreator.OpenAi,
seedream_4: ModelCreator.Bytedance,
seedream_4p5: ModelCreator.Bytedance,
seedream_5_lite: ModelCreator.Bytedance,
qwen_edit_2511_angles: ModelCreator.Alibaba,
flux_2_lora_angles: ModelCreator.BlackForestLabs,
flux_pro_kontext_max: ModelCreator.BlackForestLabs,
flux_pro_1: ModelCreator.BlackForestLabs,
flux_dev_juggernaut: ModelCreator.BlackForestLabs,
};
return values[tauriId];
};
// ── Presentation ────────────────────────────────────────────────────────────
export const imageFullName = (tauriId: string): string | undefined => {
const values: Record<string, string> = {
grok_imagine_image: "Grok",
midjourney: "Midjourney",
qwen_edit_2511_angles: "Qwen Edit 2511 Angles",
flux_2_lora_angles: "Flux 2 LoRA Angles",
flux_pro_kontext_max: "Flux Pro Kontext Max",
flux_pro_1: "Flux Pro Inpaint",
flux_dev_juggernaut: "Flux Dev Juggernaut Inpaint",
};
return values[tauriId];
};
// Only where the picker name differs from `fullName` (the caller falls back
// to `imageFullName`).
export const imageSelectorName = (tauriId: string): string | undefined => {
const values: Record<string, string> = {
flux_pro_1: "Flux Pro (Inpainting)",
flux_dev_juggernaut: "Flux Dev Juggernaut",
};
return values[tauriId];
};
export const imageSelectorDescription = (
tauriId: string,
): string | undefined => {
const values: Record<string, string> = {
grok_imagine_image: "Fast af",
midjourney: "Stunning style and quality",
flux_pro_1p1_ultra: "Higher quality model",
flux_pro_1p1: "High quality model",
flux_1_dev: "Fast, but lower quality",
flux_1_schnell: "Fastest image gen, but lowest quality",
nano_banana: "Fast instructive editing",
nano_banana_2: "Fast instructive editing",
nano_banana_pro: "Powerful instructive editing",
gpt_image_1: "Slow, but super smart",
gpt_image_1p5: "Faster, improved",
gpt_image_2: "Smart with great text support",
seedream_4: "Fast",
seedream_4p5: "Fast",
seedream_5_lite: "Fast",
qwen_edit_2511_angles: "Angle manipulation with optional prompt",
flux_2_lora_angles: "Angle manipulation",
flux_pro_kontext_max: "Fast instructive editing",
flux_pro_1: "Fast inpainting",
flux_dev_juggernaut: "Fast inpainting, low quality",
};
return values[tauriId];
};
export const imageSelectorBadges = (tauriId: string): string[] | undefined => {
const values: Record<string, string[]> = {
grok_imagine_image: ["10 sec."],
midjourney: ["45 sec."],
flux_pro_1p1_ultra: ["35 sec."],
flux_pro_1p1: ["10 sec."],
flux_1_dev: ["10 sec."],
flux_1_schnell: ["10 sec."],
nano_banana: ["25 sec."],
nano_banana_2: ["25 sec."],
nano_banana_pro: ["30 sec."],
gpt_image_1: ["60 sec."],
gpt_image_1p5: ["60 sec."],
gpt_image_2: ["2 min."],
seedream_4: ["60 sec."],
seedream_4p5: ["60 sec."],
seedream_5_lite: ["60 sec."],
qwen_edit_2511_angles: ["30 sec."],
flux_2_lora_angles: ["30 sec."],
flux_pro_kontext_max: ["20 sec."],
flux_pro_1: ["30 sec."],
flux_dev_juggernaut: ["10 sec."],
};
return values[tauriId];
};
export const imageTags = (tauriId: string): ModelTag[] | undefined => {
const values: Record<string, ModelTag[]> = {
nano_banana: [ModelTag.InstructiveEdit],
nano_banana_2: [ModelTag.InstructiveEdit],
nano_banana_pro: [ModelTag.InstructiveEdit],
gpt_image_1: [ModelTag.InstructiveEdit],
gpt_image_1p5: [ModelTag.InstructiveEdit],
gpt_image_2: [ModelTag.InstructiveEdit],
seedream_4: [ModelTag.InstructiveEdit],
seedream_4p5: [ModelTag.InstructiveEdit],
seedream_5_lite: [ModelTag.InstructiveEdit],
flux_pro_kontext_max: [ModelTag.InstructiveEdit],
};
return values[tauriId];
};
// Milliseconds; only where it differs from the `Model` default (20000).
export const imageProgressBarTime = (tauriId: string): number | undefined => {
const values: Record<string, number> = {
grok_imagine_image: 10000,
midjourney: 45000,
flux_pro_1p1_ultra: 35000,
flux_pro_1p1: 10000,
flux_1_dev: 10000,
flux_1_schnell: 10000,
nano_banana: 25000,
nano_banana_2: 25000,
nano_banana_pro: 25000,
gpt_image_1: 60000,
gpt_image_1p5: 60000,
gpt_image_2: 120000,
seedream_4: 60000,
seedream_4p5: 60000,
seedream_5_lite: 60000,
qwen_edit_2511_angles: 30000,
flux_2_lora_angles: 30000,
flux_pro_1: 30000,
flux_dev_juggernaut: 10000,
};
return values[tauriId];
};
// ── Desktop-native provider knowledge ───────────────────────────────────────
// Only models whose overlay entry explicitly set `providers:`; everything
// else falls back to `[GenerationProvider.Artcraft]` via `getProviders()`.
export const imageProviders = (
tauriId: string,
): GenerationProvider[] | undefined => {
const values: Record<string, GenerationProvider[]> = {
grok_imagine_image: [GenerationProvider.Grok],
midjourney: [GenerationProvider.Midjourney],
flux_1_dev: [GenerationProvider.Artcraft, GenerationProvider.Fal],
flux_1_schnell: [GenerationProvider.Artcraft, GenerationProvider.Fal],
nano_banana_2: [GenerationProvider.Artcraft, GenerationProvider.Fal],
nano_banana_pro: [GenerationProvider.Artcraft, GenerationProvider.Fal],
gpt_image_1: [GenerationProvider.Artcraft, GenerationProvider.Sora],
};
return values[tauriId];
};
// ── Page-subsetting flags ───────────────────────────────────────────────────
// Only entries that explicitly opt OUT of text-to-image (default is true).
export const imageCanTextToImage = (tauriId: string): boolean | undefined => {
const values: Record<string, boolean> = {
qwen_edit_2511_angles: false,
flux_2_lora_angles: false,
flux_pro_kontext_max: false,
flux_pro_1: false,
flux_dev_juggernaut: false,
};
return values[tauriId];
};
// Only `true` entries (default is false).
export const imageCanEditImages = (tauriId: string): boolean | undefined => {
const values: Record<string, boolean> = {
nano_banana: true,
nano_banana_2: true,
nano_banana_pro: true,
gpt_image_1: true,
gpt_image_1p5: true,
gpt_image_2: true,
seedream_4: true,
seedream_4p5: true,
seedream_5_lite: true,
flux_pro_kontext_max: true,
flux_pro_1: true,
flux_dev_juggernaut: true,
};
return values[tauriId];
};
// Only `true` entries (default is false).
export const imageUsesInpaintingMask = (
tauriId: string,
): boolean | undefined => {
const values: Record<string, boolean> = {
flux_pro_1: true,
flux_dev_juggernaut: true,
};
return values[tauriId];
};
// Only `true` entries (default is false).
export const imageEditingIsInpainting = (
tauriId: string,
): boolean | undefined => {
const values: Record<string, boolean> = {
flux_pro_1: true,
flux_dev_juggernaut: true,
};
return values[tauriId];
};
// Only `true` entries (default is false).
export const imageCanEditAngles = (tauriId: string): boolean | undefined => {
const values: Record<string, boolean> = {
qwen_edit_2511_angles: true,
flux_2_lora_angles: true,
};
return values[tauriId];
};
// Finite values only; only where it differs from the `Model` default (3000).
export const imageMaxPromptLength = (tauriId: string): number | undefined => {
const values: Record<string, number> = {
grok_imagine_image: 8000,
// No practical prompt limit for these models.
nano_banana: Infinity,
nano_banana_2: Infinity,
nano_banana_pro: Infinity,
gpt_image_1: Infinity,
gpt_image_1p5: Infinity,
gpt_image_2: Infinity,
midjourney: 6000,
flux_pro_1p1_ultra: 4000,
flux_pro_1p1: 4000,
flux_1_dev: 4000,
flux_1_schnell: 4000,
seedream_4: 4000,
seedream_4p5: 4000,
seedream_5_lite: 4000,
qwen_edit_2511_angles: 800,
flux_2_lora_angles: 4000,
flux_pro_kontext_max: 4000,
flux_pro_1: 4000,
flux_dev_juggernaut: 4000,
};
return values[tauriId];
};
// ── Capability fallbacks (the API may not provide these per-model) ──────────
// Only where it differs from the fallback (4).
export const imageMaxGenerationCount = (
tauriId: string,
): number | undefined => {
const values: Record<string, number> = {
// NB: Grok only supports 6 images at a time
grok_imagine_image: 6,
// NB: Fal only allows one image for some reason!
flux_pro_1: 1,
};
return values[tauriId];
};
// Only where it differs from the fallback (1).
export const imageDefaultGenerationCount = (
tauriId: string,
): number | undefined => {
const values: Record<string, number> = {
grok_imagine_image: 6,
midjourney: 4,
flux_pro_kontext_max: 4,
flux_dev_juggernaut: 4,
};
return values[tauriId];
};
export const imagePredefinedGenerationCounts = (
tauriId: string,
): number[] | undefined => {
const values: Record<string, number[]> = {
grok_imagine_image: [6],
midjourney: [4],
};
return values[tauriId];
};
// Only `true` entries (default is false).
export const imageCanUseImagePrompt = (
tauriId: string,
): boolean | undefined => {
const values: Record<string, boolean> = {
flux_pro_1p1_ultra: true,
flux_pro_1p1: true,
flux_1_dev: true,
flux_1_schnell: true,
qwen_edit_2511_angles: true,
flux_2_lora_angles: true,
};
return values[tauriId];
};
// Only where it differs from the default (1).
export const imageMaxImagePromptCount = (
tauriId: string,
): number | undefined => {
const values: Record<string, number> = {
grok_imagine_image: 6,
midjourney: 6,
flux_pro_1p1_ultra: 6,
flux_pro_1p1: 6,
flux_1_dev: 6,
flux_1_schnell: 6,
nano_banana: 6,
nano_banana_2: 6,
nano_banana_pro: 4,
gpt_image_1: 6,
gpt_image_1p5: 6,
gpt_image_2: 6,
seedream_4: 6,
seedream_4p5: 6,
seedream_5_lite: 6,
};
return values[tauriId];
};
export const imageAspectRatios = (
tauriId: string,
): CommonAspectRatio[] | undefined => {
const values: Record<string, CommonAspectRatio[]> = {
grok_imagine_image: [
CommonAspectRatio.Square,
CommonAspectRatio.WideThreeByTwo,
CommonAspectRatio.TallTwoByThree,
],
qwen_edit_2511_angles: [
CommonAspectRatio.Square,
CommonAspectRatio.SquareHd,
CommonAspectRatio.WideFourByThree,
CommonAspectRatio.WideSixteenByNine,
CommonAspectRatio.TallThreeByFour,
CommonAspectRatio.TallNineBySixteen,
],
flux_2_lora_angles: [
CommonAspectRatio.Square,
CommonAspectRatio.SquareHd,
CommonAspectRatio.WideFourByThree,
CommonAspectRatio.WideSixteenByNine,
CommonAspectRatio.TallThreeByFour,
CommonAspectRatio.TallNineBySixteen,
],
};
return values[tauriId];
};
export const imageDefaultAspectRatio = (
tauriId: string,
): CommonAspectRatio | undefined => {
const values: Record<string, CommonAspectRatio> = {
grok_imagine_image: CommonAspectRatio.Square,
qwen_edit_2511_angles: CommonAspectRatio.SquareHd,
flux_2_lora_angles: CommonAspectRatio.SquareHd,
};
return values[tauriId];
};
// Fully served by the omni API — delete this function once static fallbacks
// are no longer needed.
export const imageResolutions = (
tauriId: string,
): CommonResolution[] | undefined => {
const values: Record<string, CommonResolution[]> = {};
return values[tauriId];
};
// No overlay entry set an explicit default; `ImageModel` falls back to
// `resolutions[0]`.
export const imageDefaultResolution = (
tauriId: string,
): CommonResolution | undefined => {
const values: Record<string, CommonResolution> = {};
return values[tauriId];
};
// Fully served by the omni API — delete this function once static fallbacks
// are no longer needed.
export const imageQualityOptions = (
tauriId: string,
): CommonQuality[] | undefined => {
const values: Record<string, CommonQuality[]> = {};
return values[tauriId];
};
// Fully served by the omni API — delete this function once static fallbacks
// are no longer needed.
export const imageDefaultQuality = (
tauriId: string,
): CommonQuality | undefined => {
const values: Record<string, CommonQuality> = {};
return values[tauriId];
};
@@ -0,0 +1,511 @@
// Frontend-only per-feature data for video models, keyed by `tauriId`.
//
// Each exported function is a sparse lookup: a model has an entry ONLY when
// its value differs from the class default (see `VideoModel` / `Model`), so
// every feature is individually inspectable and deletable once the backend
// omni listing serves it. `buildModelsFromListing.ts` merges the API
// capability fields over these values at runtime.
import { GenerationProvider } from "@storyteller/api-enums";
import { ModelCreator } from "../classes/metadata/ModelCreator.js";
import { ModelTag } from "../classes/metadata/ModelTag.js";
import { SizeIconOption, SizeOption } from "../classes/metadata/SizeOption.js";
// All video model tauriIds, in picker (list) order.
export const KNOWN_VIDEO_TAURI_IDS: string[] = [
"grok_imagine_video",
"grok_imagine_video_1p5",
"kling_1p6_pro",
"kling_2p1_pro",
"kling_2p1_master",
"kling_2p5_turbo_pro",
"kling_2p6_pro",
"kling_3p0_standard",
"kling_3p0_pro",
"happy_horse_1p0",
"seedance_1p0_lite",
"seedance_1p5_pro",
"seedance_2p0",
"seedance_2p0_fast",
"sora_2",
"sora_2_pro",
"veo_2",
"veo_3",
"veo_3_fast",
"veo_3p1",
"veo_3p1_fast",
"switch_x",
];
// Models the backend omni listing does not know about; the loader appends
// these after the backend-driven entries.
export const FRONTEND_ONLY_VIDEO_TAURI_IDS: string[] = ["switch_x"];
// ── Identity ────────────────────────────────────────────────────────────────
// Frontend `id` where it historically diverged from `tauriId` (BY_ID lookups
// and generation history depend on these).
export const videoLegacyId = (tauriId: string): string | undefined => {
const values: Record<string, string> = {
grok_imagine_video: "grok_video",
kling_1p6_pro: "kling_1_6_pro",
kling_2p1_pro: "kling_2_1_pro",
kling_2p1_master: "kling_2_1_master",
seedance_1p0_lite: "seedance_1_0_lite",
};
return values[tauriId];
};
export const videoCreator = (tauriId: string): ModelCreator | undefined => {
const values: Record<string, ModelCreator> = {
grok_imagine_video: ModelCreator.Grok,
grok_imagine_video_1p5: ModelCreator.Grok,
kling_1p6_pro: ModelCreator.Kling,
kling_2p1_pro: ModelCreator.Kling,
kling_2p1_master: ModelCreator.Kling,
kling_2p5_turbo_pro: ModelCreator.Kling,
kling_2p6_pro: ModelCreator.Kling,
kling_3p0_standard: ModelCreator.Kling,
kling_3p0_pro: ModelCreator.Kling,
happy_horse_1p0: ModelCreator.Alibaba,
seedance_1p0_lite: ModelCreator.Bytedance,
seedance_1p5_pro: ModelCreator.Bytedance,
seedance_2p0: ModelCreator.Bytedance,
seedance_2p0_fast: ModelCreator.Bytedance,
sora_2: ModelCreator.OpenAi,
sora_2_pro: ModelCreator.OpenAi,
veo_2: ModelCreator.Google,
veo_3: ModelCreator.Google,
veo_3_fast: ModelCreator.Google,
veo_3p1: ModelCreator.Google,
veo_3p1_fast: ModelCreator.Google,
switch_x: ModelCreator.Beeble,
};
return values[tauriId];
};
// ── Presentation ────────────────────────────────────────────────────────────
export const videoFullName = (tauriId: string): string | undefined => {
const values: Record<string, string> = {
kling_2p1_pro: "Kling 2.1 Pro",
kling_2p1_master: "Kling 2.1 Master",
kling_3p0_standard: "Kling 3.0 Standard",
kling_3p0_pro: "Kling 3.0 Pro",
seedance_1p0_lite: "Seedance 1.0 Lite",
sora_2: "Sora 2",
sora_2_pro: "Sora 2 Pro",
veo_2: "Google Veo 2",
veo_3: "Google Veo 3",
veo_3p1: "Google Veo 3.1",
veo_3p1_fast: "Google Veo 3.1 Fast",
switch_x: "Beeble SwitchX",
};
return values[tauriId];
};
// Only where the picker name differs from `fullName` (the caller falls back
// to `videoFullName`). Currently every video model uses its full name.
export const videoSelectorName = (tauriId: string): string | undefined => {
const values: Record<string, string> = {};
return values[tauriId];
};
export const videoSelectorDescription = (
tauriId: string,
): string | undefined => {
const values: Record<string, string> = {
grok_imagine_video: "Fastest video model",
kling_1p6_pro: "Fast video model",
kling_2p1_pro: "Fast video model",
kling_2p1_master: "Fast video model",
kling_2p5_turbo_pro: "Fast video model",
kling_2p6_pro: "Fast video model",
kling_3p0_standard: "Standard quality video model",
kling_3p0_pro: "High quality video model",
happy_horse_1p0: "High quality video model",
seedance_1p0_lite: "Fast video model",
seedance_1p5_pro: "High quality video model",
sora_2: "Smart video model",
sora_2_pro: "Smart video model",
veo_2: "Fast video model",
veo_3: "Slow, high-quality model",
veo_3_fast: "High-quality model (faster)",
veo_3p1: "Slow, high-quality model",
veo_3p1_fast: "High-quality model (faster)",
switch_x: "Relight, change location, swap objects.",
};
return values[tauriId];
};
export const videoSelectorBadges = (tauriId: string): string[] | undefined => {
const values: Record<string, string[]> = {
grok_imagine_video: ["20 sec."],
grok_imagine_video_1p5: ["Preview"],
kling_1p6_pro: ["2 min."],
kling_2p1_pro: ["2 min."],
kling_2p1_master: ["2 min."],
kling_2p5_turbo_pro: ["2 min."],
kling_2p6_pro: ["2 min."],
kling_3p0_standard: ["2 min."],
kling_3p0_pro: ["2 min."],
happy_horse_1p0: ["2 min."],
seedance_1p0_lite: ["2 min."],
seedance_1p5_pro: ["2 min."],
seedance_2p0: ["~15 min."],
seedance_2p0_fast: ["~5 min."],
sora_2: ["2 min."],
sora_2_pro: ["2 min."],
veo_2: ["2 min."],
veo_3: ["2 min."],
veo_3_fast: ["2 min."],
veo_3p1: ["2 min."],
veo_3p1_fast: ["2 min."],
switch_x: ["5 min."],
};
return values[tauriId];
};
// Currently no video model declares tags.
export const videoTags = (tauriId: string): ModelTag[] | undefined => {
const values: Record<string, ModelTag[]> = {};
return values[tauriId];
};
// Milliseconds; only where it differs from the `Model` default (20000).
export const videoProgressBarTime = (tauriId: string): number | undefined => {
const values: Record<string, number> = {
grok_imagine_video: 50000,
grok_imagine_video_1p5: 50000,
kling_1p6_pro: 300000,
kling_2p1_pro: 300000,
kling_2p1_master: 300000,
kling_2p5_turbo_pro: 300000,
kling_2p6_pro: 300000,
kling_3p0_standard: 300000,
kling_3p0_pro: 300000,
happy_horse_1p0: 300000,
seedance_1p0_lite: 100000,
seedance_1p5_pro: 180000,
seedance_2p0: 900000, // ~15 minutes
seedance_2p0_fast: 300000, // ~5 minutes
sora_2: 100000,
sora_2_pro: 100000,
veo_2: 100000,
veo_3: 100000,
veo_3_fast: 100000,
veo_3p1: 100000,
veo_3p1_fast: 100000,
switch_x: 5 * 60 * 1000,
};
return values[tauriId];
};
// ── Desktop-native provider knowledge ───────────────────────────────────────
// Only models whose overlay entry explicitly set `providers:`; everything
// else falls back to `[GenerationProvider.Artcraft]` via `getProviders()`.
export const videoProviders = (
tauriId: string,
): GenerationProvider[] | undefined => {
const values: Record<string, GenerationProvider[]> = {
grok_imagine_video: [GenerationProvider.Grok],
// Served via Artcraft (storyteller-web / artcraft_router) for now, even
// though the model is made by xAI. See generate_video_command.rs.
grok_imagine_video_1p5: [GenerationProvider.Artcraft],
sora_2: [GenerationProvider.Artcraft, GenerationProvider.Sora],
};
return values[tauriId];
};
// ── Prompting ───────────────────────────────────────────────────────────────
// Finite values only; only where it differs from the `Model` default (3000).
export const videoMaxPromptLength = (tauriId: string): number | undefined => {
const values: Record<string, number> = {
grok_imagine_video: 4096,
// No practical prompt limit for these models.
seedance_2p0: Infinity,
seedance_2p0_fast: Infinity,
kling_1p6_pro: 2500,
kling_2p1_pro: 2500,
kling_2p1_master: 2500,
kling_2p5_turbo_pro: 2500,
kling_2p6_pro: 2500,
kling_3p0_standard: 2500,
kling_3p0_pro: 2500,
happy_horse_1p0: 2500,
sora_2: 2000,
sora_2_pro: 2000,
veo_2: 5000,
veo_3: 5000,
veo_3_fast: 5000,
veo_3p1: 5000,
veo_3p1_fast: 5000,
switch_x: 2000,
};
return values[tauriId];
};
// Only entries that explicitly opt OUT (default is true).
export const videoSupportsSystemPrompt = (
tauriId: string,
): boolean | undefined => {
const values: Record<string, boolean> = {
seedance_1p5_pro: false,
seedance_2p0: false,
seedance_2p0_fast: false,
};
return values[tauriId];
};
// Only `true` entries (default is false).
export const videoGenerateWithSound = (
tauriId: string,
): boolean | undefined => {
const values: Record<string, boolean> = {
kling_3p0_standard: true,
kling_3p0_pro: true,
happy_horse_1p0: true,
veo_3: true,
veo_3p1: true,
veo_3p1_fast: true,
};
return values[tauriId];
};
// Only entries that explicitly opt OUT of text-to-video (default is true).
// Fully served by the omni API — delete this function once static fallbacks
// are no longer needed.
export const videoTextToVideoSupported = (
tauriId: string,
): boolean | undefined => {
const values: Record<string, boolean> = {};
return values[tauriId];
};
// ── Keyframes / input images ────────────────────────────────────────────────
// Only `true` entries.
export const videoStartFrame = (tauriId: string): boolean | undefined => {
const values: Record<string, boolean> = {
kling_2p1_pro: true,
kling_2p1_master: true,
kling_3p0_standard: true,
kling_3p0_pro: true,
seedance_1p0_lite: true,
veo_2: true,
veo_3: true,
veo_3p1: true,
veo_3p1_fast: true,
};
return values[tauriId];
};
// Only `true` entries.
export const videoEndFrame = (tauriId: string): boolean | undefined => {
const values: Record<string, boolean> = {
kling_2p1_pro: true,
kling_3p0_standard: true,
kling_3p0_pro: true,
};
return values[tauriId];
};
// Only `true` entries.
export const videoRequiresImage = (tauriId: string): boolean | undefined => {
const values: Record<string, boolean> = {
grok_imagine_video: true,
grok_imagine_video_1p5: true,
kling_1p6_pro: true,
kling_2p1_pro: true,
kling_2p1_master: true,
kling_2p6_pro: true,
seedance_1p0_lite: true,
veo_2: true,
veo_3: true,
veo_3_fast: true,
veo_3p1: true,
veo_3p1_fast: true,
switch_x: true,
};
return values[tauriId];
};
// ── Size / aspect ratio ─────────────────────────────────────────────────────
export const videoSizeOptions = (
tauriId: string,
): SizeOption[] | undefined => {
const values: Record<string, SizeOption[]> = {
kling_3p0_standard: [
{
tauriValue: "wide_sixteen_by_nine",
textLabel: "16:9",
icon: SizeIconOption.Landscape16x9,
},
{
tauriValue: "square",
textLabel: "1:1",
icon: SizeIconOption.Square,
},
{
tauriValue: "tall_nine_by_sixteen",
textLabel: "9:16",
icon: SizeIconOption.Portrait9x16,
},
],
kling_3p0_pro: [
{
tauriValue: "wide_sixteen_by_nine",
textLabel: "16:9",
icon: SizeIconOption.Landscape16x9,
},
{
tauriValue: "square",
textLabel: "1:1",
icon: SizeIconOption.Square,
},
{
tauriValue: "tall_nine_by_sixteen",
textLabel: "9:16",
icon: SizeIconOption.Portrait9x16,
},
],
sora_2: [
{
tauriValue: "landscape",
textLabel: "Landscape",
icon: SizeIconOption.Landscape,
},
{
tauriValue: "portrait",
textLabel: "Portrait",
icon: SizeIconOption.Portrait,
},
],
sora_2_pro: [
{
tauriValue: "landscape",
textLabel: "Landscape",
icon: SizeIconOption.Landscape,
},
{
tauriValue: "portrait",
textLabel: "Portrait",
icon: SizeIconOption.Portrait,
},
],
};
return values[tauriId];
};
// Only `true` entries (default is false).
export const videoSupportsCommonAspectRatio = (
tauriId: string,
): boolean | undefined => {
const values: Record<string, boolean> = {
kling_3p0_standard: true,
kling_3p0_pro: true,
};
return values[tauriId];
};
// ── Duration / resolution ───────────────────────────────────────────────────
export const videoDurationOptions = (
tauriId: string,
): number[] | undefined => {
const values: Record<string, number[]> = {
kling_3p0_standard: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
kling_3p0_pro: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
happy_horse_1p0: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
seedance_1p5_pro: [4, 5, 6, 7, 8, 9, 10, 11, 12],
seedance_2p0: [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
seedance_2p0_fast: [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
};
return values[tauriId];
};
export const videoDefaultDuration = (tauriId: string): number | undefined => {
const values: Record<string, number> = {
kling_3p0_standard: 5,
kling_3p0_pro: 5,
};
return values[tauriId];
};
// Fully served by the omni API — delete this function once static fallbacks
// are no longer needed.
export const videoResolutionOptions = (
tauriId: string,
): string[] | undefined => {
const values: Record<string, string[]> = {};
return values[tauriId];
};
// Fully served by the omni API — delete this function once static fallbacks
// are no longer needed.
export const videoDefaultResolution = (
tauriId: string,
): string | undefined => {
const values: Record<string, string> = {};
return values[tauriId];
};
// ── Reference mode ──────────────────────────────────────────────────────────
// Only `true` entries.
// Fully served by the omni API — delete this function once static fallbacks
// are no longer needed.
export const videoSupportsReferenceMode = (
tauriId: string,
): boolean | undefined => {
const values: Record<string, boolean> = {};
return values[tauriId];
};
// Fully served by the omni API — delete this function once static fallbacks
// are no longer needed.
export const videoMaxReferenceImages = (
tauriId: string,
): number | undefined => {
const values: Record<string, number> = {};
return values[tauriId];
};
// Fully served by the omni API — delete this function once static fallbacks
// are no longer needed.
export const videoMaxReferenceVideos = (
tauriId: string,
): number | undefined => {
const values: Record<string, number> = {};
return values[tauriId];
};
// Fully served by the omni API — delete this function once static fallbacks
// are no longer needed.
export const videoMaxVideoRefDuration = (
tauriId: string,
): number | undefined => {
const values: Record<string, number> = {};
return values[tauriId];
};
// Fully served by the omni API — delete this function once static fallbacks
// are no longer needed.
export const videoMaxReferenceAudios = (
tauriId: string,
): number | undefined => {
const values: Record<string, number> = {};
return values[tauriId];
};
// Fully served by the omni API — delete this function once static fallbacks
// are no longer needed.
export const videoMaxAudioRefDuration = (
tauriId: string,
): number | undefined => {
const values: Record<string, number> = {};
return values[tauriId];
};
@@ -47,11 +47,12 @@ export const useModelsStore = create<ModelsStoreState>((set, get) => ({
const next: Partial<ModelsStoreState> = { isLoading: false, loaded: true };
if (imageResult.status === "fulfilled") {
const { models } = imageResult.value.payload;
const built = buildImageModelsFromListing(IMAGE_MODELS, models);
const { models, providers } = imageResult.value.payload;
const offered = providers.flatMap((p) => p.models.map((pm) => pm.model));
const built = buildImageModelsFromListing(IMAGE_MODELS, models, offered);
next.imageModels = built;
console.log(
`[models] image: backend returned ${models.length} → ${built.length} models shown`,
`[models] image: backend returned ${models.length} details, ${offered.length} offered → ${built.length} models shown`,
built.map((m) => m.tauriId),
);
} else {
@@ -59,11 +60,12 @@ export const useModelsStore = create<ModelsStoreState>((set, get) => ({
}
if (videoResult.status === "fulfilled") {
const { models } = videoResult.value.payload;
const built = buildVideoModelsFromListing(VIDEO_MODELS, models);
const { models, providers } = videoResult.value.payload;
const offered = providers.flatMap((p) => p.models.map((pm) => pm.model));
const built = buildVideoModelsFromListing(VIDEO_MODELS, models, offered);
next.videoModels = built;
console.log(
`[models] video: backend returned ${models.length} → ${built.length} models shown`,
`[models] video: backend returned ${models.length} details, ${offered.length} offered → ${built.length} models shown`,
built.map((m) => m.tauriId),
);
} else {