diff --git a/frontend/libs/components/model-selector/src/lib/classy-model-selector.tsx b/frontend/libs/components/model-selector/src/lib/classy-model-selector.tsx index aa87522920..f9fc294c45 100644 --- a/frontend/libs/components/model-selector/src/lib/classy-model-selector.tsx +++ b/frontend/libs/components/model-selector/src/lib/classy-model-selector.tsx @@ -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[]; 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) => ( { 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 ? (
@@ -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; diff --git a/frontend/libs/model-list/src/index.ts b/frontend/libs/model-list/src/index.ts index 5f26c13249..af6a2254e1 100644 --- a/frontend/libs/model-list/src/index.ts +++ b/frontend/libs/model-list/src/index.ts @@ -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"; diff --git a/frontend/libs/model-list/src/lib/lists/ImageModels.ts b/frontend/libs/model-list/src/lib/lists/ImageModels.ts index cca006ee3f..a9fd4e8c29 100644 --- a/frontend/libs/model-list/src/lib/lists/ImageModels.ts +++ b/frontend/libs/model-list/src/lib/lists/ImageModels.ts @@ -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 = new Map( IMAGE_MODELS.map((model) => [model.id, model]), diff --git a/frontend/libs/model-list/src/lib/lists/VideoModels.ts b/frontend/libs/model-list/src/lib/lists/VideoModels.ts index bc8fa13a51..db3174504a 100644 --- a/frontend/libs/model-list/src/lib/lists/VideoModels.ts +++ b/frontend/libs/model-list/src/lib/lists/VideoModels.ts @@ -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 = new Map( VIDEO_MODELS.map((model) => [model.id, model]), diff --git a/frontend/libs/model-list/src/lib/loader/buildModelsFromListing.ts b/frontend/libs/model-list/src/lib/loader/buildModelsFromListing.ts index 7ee14c6c94..c7643a1f89 100644 --- a/frontend/libs/model-list/src/lib/loader/buildModelsFromListing.ts +++ b/frontend/libs/model-list/src/lib/loader/buildModelsFromListing.ts @@ -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 = ( overlay: T[], listing: L[], + offeredModelIds: string[], alias: Record, 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(); + 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(); - // 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 ?? [], diff --git a/frontend/libs/model-list/src/lib/loader/imageModelFeatures.ts b/frontend/libs/model-list/src/lib/loader/imageModelFeatures.ts new file mode 100644 index 0000000000..25a9325dab --- /dev/null +++ b/frontend/libs/model-list/src/lib/loader/imageModelFeatures.ts @@ -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 = { + 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 = { + 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 = { + 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 = { + 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 = { + 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 = { + 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 = { + 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 = { + 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 = { + 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 = { + 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 = { + 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 = { + 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 = { + 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 = { + 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 = { + 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 = { + // 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 = { + 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 = { + 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 = { + 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 = { + 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 = { + 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 = { + 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 = {}; + 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 = {}; + 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 = {}; + 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 = {}; + return values[tauriId]; +}; diff --git a/frontend/libs/model-list/src/lib/loader/videoModelFeatures.ts b/frontend/libs/model-list/src/lib/loader/videoModelFeatures.ts new file mode 100644 index 0000000000..e0aeaa5ea4 --- /dev/null +++ b/frontend/libs/model-list/src/lib/loader/videoModelFeatures.ts @@ -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 = { + 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 = { + 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 = { + 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 = {}; + return values[tauriId]; +}; + +export const videoSelectorDescription = ( + tauriId: string, +): string | undefined => { + const values: Record = { + 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 = { + 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 = {}; + return values[tauriId]; +}; + +// Milliseconds; only where it differs from the `Model` default (20000). +export const videoProgressBarTime = (tauriId: string): number | undefined => { + const values: Record = { + 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 = { + 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 = { + 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 = { + 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 = { + 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 = {}; + return values[tauriId]; +}; + +// ── Keyframes / input images ──────────────────────────────────────────────── + +// Only `true` entries. +export const videoStartFrame = (tauriId: string): boolean | undefined => { + const values: Record = { + 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 = { + 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 = { + 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 = { + 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 = { + kling_3p0_standard: true, + kling_3p0_pro: true, + }; + return values[tauriId]; +}; + +// ── Duration / resolution ─────────────────────────────────────────────────── + +export const videoDurationOptions = ( + tauriId: string, +): number[] | undefined => { + const values: Record = { + 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 = { + 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 = {}; + 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 = {}; + 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 = {}; + 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 = {}; + 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 = {}; + 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 = {}; + 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 = {}; + 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 = {}; + return values[tauriId]; +}; diff --git a/frontend/libs/tauri-api/src/lib/models/modelsStore.ts b/frontend/libs/tauri-api/src/lib/models/modelsStore.ts index 0c6540e84c..a4f27fcfb5 100644 --- a/frontend/libs/tauri-api/src/lib/models/modelsStore.ts +++ b/frontend/libs/tauri-api/src/lib/models/modelsStore.ts @@ -47,11 +47,12 @@ export const useModelsStore = create((set, get) => ({ const next: Partial = { 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((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 {