Compare commits

..
Author SHA1 Message Date
bflatastic 3201333056 centralized models refactor 2025-08-13 23:42:56 +07:00
BFlat 93e9d423cb added more model creator icons and a generic icon (#758)
**added logos:**
- alibaba (wan video and qwen)
- fal
- higgsfield
- krea
- openart
- replicate
- tencent (logo for their AI model instead of company logo)
- tensorart
- vidu
- generic model icon

... also removed **IMAGE_MODELS_BY_LABEL** and **VIDEO_MODELS_BY_LABEL**
2025-08-12 14:36:34 +07:00
Brandon Thomas e6267eeb23 stop app from crashing from schema migration (#757) 2025-08-09 09:05:05 -04:00
BFlat 4e28d05e51 copy paste bug fixes (#756)
copy paste character works, tested cmd + c/v works on mac too.
2025-08-08 20:52:03 +07:00
30 changed files with 487 additions and 301 deletions
@@ -1,7 +1,14 @@
-- noinspection SqlDialectInspectionForFile
--
-- NB: This version needs to be bumped even if just adding or changing comments!
-- Failure to do so will cause Windows to segfault at start. (This is horrible!)
--
-- NB: `app_state_dir.rs` contains the file version number.
--
-- Migration history:
-- tasks_v1.sqlite - initial version
-- tasks_v2.sqlite - added model_type (nullable)
-- tasks_v3.sqlite - added frontend_caller, comments
CREATE TABLE tasks (
-- Task auto-incrementing primary key.
@@ -22,7 +22,9 @@ impl DataSubdir for AppStateDir{
impl AppStateDir {
pub fn get_tasks_sqlite_database_path(&self) -> PathBuf {
self.path.join("tasks_v2.sqlite")
// NB: This must be incremented on any change to the database schema (including comments).
// The database houses ephemeral content and will be migrated automatically.
self.path.join("tasks_v3.sqlite")
}
pub fn get_window_size_config_file(&self) -> PathBuf {
@@ -12,17 +12,27 @@ pub struct TaskDbConnection {
impl TaskDbConnection {
pub async fn connect_and_migrate<P: AsRef<Path>>(database_file: P) -> AnyhowResult<Self> {
match run_migrations(&database_file).await {
Ok(pool) => return Ok(Self { pool }),
Err(err) => {
error!("Error running SQLite migrations: {:?}", err);
{
match run_migrations(&database_file).await {
Ok(pool) => return Ok(Self { pool }),
Err(err) => {
error!("Error running SQLite migrations: {:?}", err);
}
}
}
info!("Deleting and recreating SQLite database at {:?}", database_file.as_ref());
info!("Deleting and recreating SQLite database at {:?}", database_file.as_ref());
if let Err(err) = std::fs::remove_file(&database_file) {
error!("Error deleting SQLite database file: {:?}", err);
if let Err(err) = std::fs::remove_file(&database_file) {
error!("Error deleting SQLite database file: {:?}", err);
}
// NB: Scope change here is an attempt to drop the open file connection for Windows.
// On Windows, we've observed that the file might still be open, preventing deletion:
// [2025-08-08][06:42:29][sqlite_tasks::connection][INFO] Deleting and recreating SQLite
// database at "C:\Users\User\Artcraft\state\tasks_v2.sqlite"
// [2025-08-08][06:42:29][sqlite_tasks::connection][ERROR] Error deleting SQLite database
// file: Os { code: 32, kind: Uncategorized, message: "The process cannot access the file
// because it is being used by another process." }
}
let pool = run_migrations(database_file).await?;
@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Qwen</title><path d="M12.604 1.34c.393.69.784 1.382 1.174 2.075a.18.18 0 00.157.091h5.552c.174 0 .322.11.446.327l1.454 2.57c.19.337.24.478.024.837-.26.43-.513.864-.76 1.3l-.367.658c-.106.196-.223.28-.04.512l2.652 4.637c.172.301.111.494-.043.77-.437.785-.882 1.564-1.335 2.34-.159.272-.352.375-.68.37-.777-.016-1.552-.01-2.327.016a.099.099 0 00-.081.05 575.097 575.097 0 01-2.705 4.74c-.169.293-.38.363-.725.364-.997.003-2.002.004-3.017.002a.537.537 0 01-.465-.271l-1.335-2.323a.09.09 0 00-.083-.049H4.982c-.285.03-.553-.001-.805-.092l-1.603-2.77a.543.543 0 01-.002-.54l1.207-2.12a.198.198 0 000-.197 550.951 550.951 0 01-1.875-3.272l-.79-1.395c-.16-.31-.173-.496.095-.965.465-.813.927-1.625 1.387-2.436.132-.234.304-.334.584-.335a338.3 338.3 0 012.589-.001.124.124 0 00.107-.063l2.806-4.895a.488.488 0 01.422-.246c.524-.001 1.053 0 1.583-.006L11.704 1c.341-.003.724.032.9.34zm-3.432.403a.06.06 0 00-.052.03L6.254 6.788a.157.157 0 01-.135.078H3.253c-.056 0-.07.025-.041.074l5.81 10.156c.025.042.013.062-.034.063l-2.795.015a.218.218 0 00-.2.116l-1.32 2.31c-.044.078-.021.118.068.118l5.716.008c.046 0 .08.02.104.061l1.403 2.454c.046.081.092.082.139 0l5.006-8.76.783-1.382a.055.055 0 01.096 0l1.424 2.53a.122.122 0 00.107.062l2.763-.02a.04.04 0 00.035-.02.041.041 0 000-.04l-2.9-5.086a.108.108 0 010-.113l.293-.507 1.12-1.977c.024-.041.012-.062-.035-.062H9.2c-.059 0-.073-.026-.043-.077l1.434-2.505a.107.107 0 000-.114L9.225 1.774a.06.06 0 00-.053-.031zm6.29 8.02c.046 0 .058.02.034.06l-.832 1.465-2.613 4.585a.056.056 0 01-.05.029.058.058 0 01-.05-.029L8.498 9.841c-.02-.034-.01-.052.028-.054l.216-.012 6.722-.012z"></path></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Fal</title><path clip-rule="evenodd" d="M15.477 0c.415 0 .749.338.788.752a7.775 7.775 0 006.985 6.984c.413.04.752.373.752.788v6.952c0 .415-.338.748-.752.788a7.775 7.775 0 00-6.985 6.984c-.04.414-.373.752-.788.752H8.525c-.416 0-.749-.338-.789-.752a7.775 7.775 0 00-6.984-6.984c-.414-.04-.752-.373-.752-.788V8.524c0-.415.338-.748.752-.788A7.775 7.775 0 007.736.752C7.776.338 8.11 0 8.526 0h6.95zM4.819 11.98a7.226 7.226 0 007.223 7.23 7.226 7.226 0 007.223-7.23c0-3.994-3.234-7.23-7.223-7.23a7.227 7.227 0 00-7.223 7.23z"></path></svg>

After

Width:  |  Height:  |  Size: 697 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M64 144C64 117.5 85.5 96 112 96L208 96C234.5 96 256 117.5 256 144L256 160L384 160L384 144C384 117.5 405.5 96 432 96L528 96C554.5 96 576 117.5 576 144L576 240C576 266.5 554.5 288 528 288L432 288C405.5 288 384 266.5 384 240L384 224L256 224L256 240C256 247.3 254.3 254.3 251.4 260.5L320 352L400 352C426.5 352 448 373.5 448 400L448 496C448 522.5 426.5 544 400 544L304 544C277.5 544 256 522.5 256 496L256 400C256 392.7 257.7 385.7 260.6 379.5L192 288L112 288C85.5 288 64 266.5 64 240L64 144z"/></svg>

After

Width:  |  Height:  |  Size: 717 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
<path d="M.03,2.19c.14.03.29.07.43.09,1.26.16,1.76-1.08,2.78-1.5,1.26-.52,2.77-.09,2.67,1.5s-1.89,2.3-2.88,3.2c-.38.34-.74.91-.36,1.4.5.64,1.25.06,1.65-.36,1.18-1.22,2.38-3.76,4.38-3.57,1.47.14,2.2,1.65,2.32,2.97h.96l.03.03v1.36h-.99c-.09,1.37-.54,3.03-1.78,3.79-.9.55-2.02.51-2.73-.31-.78-.89-.43-1.96.16-2.84.71-1.06,1.85-1.83,3.12-2.04.04-.61-.34-1.2-.94-1.35-1.15-.3-2.03.74-2.65,1.54-.79,1-1.87,2.88-3.33,2.8-1.19-.07-1.99-1.08-1.93-2.24.05-1.04,1.04-1.89,1.83-2.46.52-.38,1.71-1.05,1.81-1.72.06-.41-.18-.75-.61-.65-.59.13-1.03.85-1.49,1.22-.69.56-1.57.85-2.43.48v-1.3ZM9.73,7.28c-.28.07-.56.16-.81.3-.58.31-1.35,1.09-1.45,1.77-.08.58.45.87.95.62.87-.43,1.26-1.79,1.31-2.69Z"/>
</svg>

After

Width:  |  Height:  |  Size: 824 B

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
<defs>
<style>
.cls-1 {
fill: #010101;
}
</style>
</defs>
<path d="M9.26.37c.36.29.62.65.72,1.11.01.14.01.27.01.41t0,.06c0,.51-.07.99-.22,1.47t-.02.06c-.24.83-.68,1.56-1.25,2.21-.02.02-.04.05-.06.07-.21.23-.42.43-.66.63-.02.02-.04.03-.06.05-.87.72-1.93,1.12-3.04,1.29-.03,0-.06.01-.09.02-.63.1-1.3.08-1.85-.27-.11-.08-.21-.18-.31-.28-.02-.02-.03-.03-.05-.05-.32-.34-.43-.82-.43-1.27.02-.45.27-.87.59-1.17.36-.31.81-.47,1.29-.48.62-.01,1.16-.23,1.64-.62.02-.02.04-.03.07-.05.39-.33.67-.89.73-1.39,0-.11.02-.22.02-.33.03-.51.2-.93.57-1.29.07-.06.15-.12.23-.18.02-.01.03-.02.05-.04.63-.43,1.49-.36,2.09.06Z"/>
<path d="M8.61,6.51c.06,0,.1.02.14.06t.04.05s.03.04.04.06c.01.02.03.04.04.06.01.02.03.04.04.06.4.57.68,1.19.88,1.85t.02.05c.22.71.32,1.62-.02,2.29-.24.47-.63.72-1.11.91-.48.14-1.02.06-1.46-.17-.47-.28-.74-.67-.89-1.19-.03-.12-.04-.25-.04-.37-.02-.65-.25-1.22-.71-1.68-.06-.05-.12-.11-.19-.16-.03-.03-.05-.05-.08-.08.13-.04.25-.08.38-.12.99-.29,2.17-.79,2.86-1.61l.04-.03Z"/>
<path class="cls-1" d="M4.96.4c.34.27.63.67.69,1.1.02.29.02.58-.08.85t-.03.08c-.16.44-.5.77-.92.97-.29.13-.53.17-.85.18-.68.04-1.21.26-1.72.69-.02.02-.05.04-.07.06,0-.36,0-.71,0-1.07,0-.17,0-.33,0-.5,0-.16,0-.32,0-.48,0-.06,0-.12,0-.18,0-.63.12-1.11.56-1.58.67-.65,1.69-.66,2.43-.13Z"/>
<path d="M1.98,7.67s.07.05.1.07c.02.01.04.03.06.04.04.03.09.07.13.1.41.3.88.5,1.39.51.58.01,1.1.17,1.52.59.35.4.5.86.47,1.39-.05.49-.31.89-.67,1.21-.46.35-1.01.43-1.57.36-.47-.1-.88-.4-1.15-.79-.07-.12-.13-.23-.18-.36,0-.02-.02-.05-.03-.07-.1-.31-.09-.61-.09-.93,0-.06,0-.12,0-.18,0-.16,0-.32,0-.48,0-.16,0-.33,0-.49,0-.32,0-.64,0-.96Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
<defs>
<style>
.cls-1 {
fill-rule: evenodd;
}
</style>
</defs>
<path class="cls-1" d="M8.6,2.63c.89,0,1.75.35,2.39.99.63.63.99,1.49,1,2.38,0,.66-.2,1.3-.56,1.84-.36.55-.87.98-1.47,1.24-.6.26-1.27.34-1.91.24-.65-.11-1.25-.4-1.73-.84-.59-.5-1.19-.98-1.79-1.47l.86-.69c.84.67,1.57,1.27,1.85,1.5.39.29.87.45,1.37.45,1.23,0,2.32-1.02,2.3-2.28,0-.42-.13-.84-.35-1.19s-.54-.65-.92-.84c-.38-.19-.8-.27-1.22-.23-.42.04-.82.19-1.17.44-1.82,1.51-3.71,2.97-5.56,4.44-.75.53-1.64,0-1.64-.87C.01,6.58.01,5.42.02,4.26c0-.88.89-1.41,1.65-.88l2.03,1.64-.84.68-1.75-1.38c0,1.12,0,2.24,0,3.36,1.75-1.36,3.49-2.74,5.2-4.15.62-.57,1.44-.89,2.29-.89h0Z"/>
</svg>

After

Width:  |  Height:  |  Size: 807 B

@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Replicate</title><path d="M22 10.552v2.26h-7.932V22H11.54V10.552H22zM22 2v2.264H4.528V22H2V2h20zm0 4.276V8.54H9.296V22H6.768V6.276H22z"></path></svg>

After

Width:  |  Height:  |  Size: 313 B

@@ -1 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Tencent</title><path d="M9.976 1L24 9.8l-10.587.015L10.723 23H5.489L8.18 9.8H3.244L1 5.4h8.077L9.976 1z"></path></svg>
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Hunyuan</title><path d="M12 0c6.627 0 12 5.373 12 12s-5.373 12-12 12S0 18.627 0 12 5.373 0 12 0zm1.652 1.123l-.01-.001c.533.097 1.023.233 1.41.404 6.084 2.683 7.396 9.214 1.601 14.338a3.781 3.781 0 01-5.337-.328 3.654 3.654 0 01-.884-3.044c-1.934.6-3.295 2.305-3.524 4.45-.204 1.912.324 4.044 2.056 5.634l.245.067C10.1 22.876 11.036 23 12 23c6.075 0 11-4.925 11-11 0-5.513-4.056-10.08-9.348-10.877zM2.748 6.21c-.178.269-.348.536-.51.803l-.235.394.078-.167A10.957 10.957 0 001 12c0 4.919 3.228 9.083 7.682 10.49l.214.065C3.523 18.528 2.84 14.149 6.47 8.68A2.234 2.234 0 102.748 6.21zm10.157-5.172c4.408 1.33 3.61 5.41 2.447 6.924-.86 1.117-2.922 1.46-3.708 2.238-.666.657-1.077 1.462-1.212 2.291A5.303 5.303 0 0112 12.258a5.672 5.672 0 001.404-11.169 10.51 10.51 0 00-.5-.052z"></path></svg>

Before

Width:  |  Height:  |  Size: 282 B

After

Width:  |  Height:  |  Size: 954 B

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
<path d="M7.51,3.04q.14,0,.27,0c.1,0,.2,0,.3,0,.1,0,.21,0,.31,0,.1,0,.21,0,.31,0,.26,0,.51,0,.77,0,.29.67.58,1.34.87,2.01.15.34.29.67.44,1.01.15.35.3.7.45,1.05.06.14.12.27.18.41.08.19.17.38.25.57q.04.09.08.17c.11.25.17.42.27.69h-2.36c-.45-1.18-.89-2.37-1.3-3.56-.31.75-.58,1.51-.85,2.27-.04.13-.09.25-.13.38-.11.3-.22.61-.32.92h-2.26c.29-.76.59-1.51.91-2.26.04-.1.09-.2.13-.31.09-.21.18-.43.27-.64.12-.27.23-.55.35-.82.11-.26.22-.53.34-.79.04-.1.08-.2.13-.3q.06-.14.12-.27c.03-.08.07-.16.1-.24.07-.15.12-.28.12-.28,0,0,.14,0,.26,0Z"/>
<path d="M1.65,2.96h3.82v1.65h-1.74v4.43H1.65V2.96Z"/>
<path d="M0,2.96h1.3v1.74H0v-1.74Z"/>
<path d="M8.25,6.78h.17c.23.72.46,1.43.7,2.17h-1.56c.21-.73.45-1.45.7-2.17Z"/>
</svg>

After

Width:  |  Height:  |  Size: 854 B

@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Vidu</title><path d="M20.607 2.247c-2.917-.966-5.426 1.084-6.011 2.96 0 0-2.105 6.76-3.002 9.58-.428 1.346-1.489 3.548-3.487 3.548-1.627 0-2.463-1.527-2.816-2.437L2.865 9.431c-.281-.681.013-2.04 1.14-2.447 1.204-.432 1.978.575 2.178 1.11l3.022 7.74c.72-.928 1.178-2.438 1.476-3.507l-1.984-5.21C7.756 4.686 5.267 3.58 2.962 4.43 1.095 5.118-.702 7.474.275 10.435l2.504 6.44c.38.976 1.881 4.163 5.275 4.163 4.073 0 5.601-3.473 6.449-6.218.424-1.373 2.749-8.797 2.749-8.797.338-1.109 1.71-1.428 2.568-1.148.605.196 1.698 1.031 1.345 2.325-.066.236-1.92 6.209-2.604 8.026-.357.948-1.262 3.006-3.324 2.72-.628 1.39-1.15 2.199-1.94 2.925 2.572 1.218 6.32-.009 7.898-4.776.586-1.773 2.644-8.166 2.644-8.166.598-1.963-.469-4.768-3.232-5.682z"></path></svg>

After

Width:  |  Height:  |  Size: 912 B

@@ -16,11 +16,15 @@ import { captureStageImageBitmap } from "./hooks/useUpdateSnapshot";
import { ContextMenuContainer } from "./components/ui/ContextMenu";
import { FalBackgroundRemoval } from "@storyteller/tauri-api";
import { EnqueueImageBgRemoval } from "@storyteller/tauri-api";
import { getCreatorIcon, IMAGE_MODELS_BY_LABEL, ModelCreator, ModelInfo } from "@storyteller/model-list";
import { instructiveImageEditModels, ModelCategory, ModelSelector, useModelSelectorStore } from "@storyteller/ui-model-selector";
import { ModelInfo } from "@storyteller/model-list";
import {
instructiveImageEditModels,
ModelCategory,
ModelSelector,
useModelSelectorStore,
} from "@storyteller/ui-model-selector";
import { useCanvasBgRemovedEvent } from "@storyteller/tauri-api";
export const DecodeBase64ToImage = async (
base64String: string,
): Promise<ImageBitmap> => {
@@ -60,7 +64,9 @@ const PageDraw = () => {
instructiveImageEditModels[0]?.label;
const selectedModelInfo: ModelInfo | undefined =
IMAGE_MODELS_BY_LABEL[selectedModel];
instructiveImageEditModels.find(
(m) => m.label === selectedModel,
)?.modelInfo;
useDeleteHotkeys({ onDelete: store.deleteSelectedItems });
useUndoRedoHotkeys({ undo: store.undo, redo: store.redo });
@@ -86,7 +92,7 @@ const PageDraw = () => {
store.finishRemoveBackground(
nodeId,
event.media_token,
event.image_cdn_url
event.image_cdn_url,
);
});
@@ -78,7 +78,6 @@ import { LoginModal, useLoginModalStore } from "@storyteller/ui-login-modal";
import PageDraw from "../PageDraw/PageDraw";
import { useTabStore } from "../Stores/TabState";
import PageEdit from "../PageEdit/PageEdit";
import { IMAGE_MODELS_BY_LABEL } from "@storyteller/model-list";
import { ModelInfo } from "@storyteller/model-list";
export const PageEditor = () => {
@@ -117,7 +116,9 @@ export const PageEditor = () => {
instructiveImageEditModels[0]?.label;
const selectedModelInfo: ModelInfo | undefined =
IMAGE_MODELS_BY_LABEL[selectedModel];
instructiveImageEditModels.find(
(m) => m.label === selectedModel,
)?.modelInfo;
const height =
dndTimelineHeight.value > -1
@@ -13,7 +13,6 @@ interface TextToImageProps {
imageMediaId?: string;
imageUrl?: string;
}
import { IMAGE_MODELS_BY_LABEL } from "@storyteller/model-list";
import { ModelInfo } from "@storyteller/model-list";
const TextToImage = ({ imageMediaId, imageUrl }: TextToImageProps) => {
@@ -24,8 +23,9 @@ const TextToImage = ({ imageMediaId, imageUrl }: TextToImageProps) => {
selectedModels[ModelCategory.TextToImage] ||
imageGenerationModels[0]?.label;
const selectedModelInfo: ModelInfo | undefined =
IMAGE_MODELS_BY_LABEL[selectedModel];
const selectedModelInfo: ModelInfo | undefined = imageGenerationModels.find(
(m) => m.label === selectedModel,
)?.modelInfo;
const jobContext: JobContextType = {
jobTokens: [],
@@ -8,7 +8,6 @@ import {
useModelSelectorStore,
videoGenerationModels,
} from "@storyteller/ui-model-selector";
import { VIDEO_MODELS_BY_LABEL } from "@storyteller/model-list";
import { ModelInfo } from "@storyteller/model-list";
interface ImageToVideoProps {
@@ -24,8 +23,9 @@ const ImageToVideo = ({ imageMediaId, imageUrl }: ImageToVideoProps) => {
selectedModels[ModelCategory.ImageToVideo] ||
videoGenerationModels[0]?.label;
const selectedModelInfo: ModelInfo | undefined =
VIDEO_MODELS_BY_LABEL[selectedModel];
const selectedModelInfo: ModelInfo | undefined = videoGenerationModels.find(
(m) => m.label === selectedModel,
)?.modelInfo;
const jobContext: JobContextType = {
jobTokens: [],
@@ -2,144 +2,52 @@ import type { PopoverItem } from "@storyteller/ui-popover";
import { faClock, faFilm, faImage } from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
IMAGE_MODELS,
VIDEO_MODELS,
getCreatorIcon,
ModelCreator,
getModelsByCategory,
getInstructiveImageEditModels,
} from "@storyteller/model-list";
export type ModelList = Omit<PopoverItem, "selected">[];
const withIcon = (creatorIcon: any, fallback: any) => creatorIcon || fallback;
const buildItems = (
models: {
label: string;
description?: string;
badges?: { label: string }[];
info: { creator: any };
}[],
fallbackIcon: any
) =>
models.map((m) => ({
label: m.label,
icon: withIcon(getCreatorIcon((m as any).info.creator), fallbackIcon),
description: (m as any).description,
badges: (m as any).badges?.map((b: any) => ({
label: b.label,
icon: <FontAwesomeIcon icon={faClock} />,
})),
modelInfo: (m as any).info,
}));
const imageModels = getModelsByCategory("image");
const videoModels = getModelsByCategory("video");
const instructiveModels = getInstructiveImageEditModels();
export const allModels = {
video: [
{
label: "Kling 1.6 Pro",
icon: getCreatorIcon(ModelCreator.Kling) || (
<FontAwesomeIcon icon={faFilm} className="h-4 w-4" />
),
description: "Good quality model",
badges: [{ label: "2 min.", icon: <FontAwesomeIcon icon={faClock} /> }],
modelInfo: VIDEO_MODELS.kling_1_6_pro,
},
{
label: "Kling 2.1 Pro",
icon: getCreatorIcon(ModelCreator.Kling) || (
<FontAwesomeIcon icon={faFilm} className="h-4 w-4" />
),
description: "High quality model",
badges: [{ label: "2 min.", icon: <FontAwesomeIcon icon={faClock} /> }],
modelInfo: VIDEO_MODELS.kling_2_1_pro,
},
{
label: "Kling 2.1 Master",
icon: getCreatorIcon(ModelCreator.Kling) || (
<FontAwesomeIcon icon={faFilm} className="h-4 w-4" />
),
description: "Master quality model ($$)",
badges: [{ label: "2 min.", icon: <FontAwesomeIcon icon={faClock} /> }],
modelInfo: VIDEO_MODELS.kling_2_1_master,
},
{
label: "Seedance 1.0 Lite",
icon: getCreatorIcon(ModelCreator.Bytedance) || (
<FontAwesomeIcon icon={faFilm} className="h-4 w-4" />
),
description: "Fast and high-quality model",
badges: [{ label: "2 min.", icon: <FontAwesomeIcon icon={faClock} /> }],
modelInfo: VIDEO_MODELS.seedance_1_0_lite,
},
{
label: "Google Veo 2",
icon: getCreatorIcon(ModelCreator.Google) || (
<FontAwesomeIcon icon={faFilm} className="h-4 w-4" />
),
description: "Fast and high-quality model",
badges: [{ label: "2 min.", icon: <FontAwesomeIcon icon={faClock} /> }],
modelInfo: VIDEO_MODELS.veo_2,
},
],
image: [
{
label: "Flux Pro 1.1 Ultra",
icon: getCreatorIcon(ModelCreator.BlackForestLabs) || (
<FontAwesomeIcon icon={faImage} className="h-4 w-4" />
),
description: "High quality model",
badges: [{ label: "15 sec.", icon: <FontAwesomeIcon icon={faClock} /> }],
modelInfo: IMAGE_MODELS.flux_pro_1_1_ultra,
},
{
label: "Flux Pro 1.1",
icon: getCreatorIcon(ModelCreator.BlackForestLabs) || (
<FontAwesomeIcon icon={faImage} className="h-4 w-4" />
),
description: "High quality model",
badges: [{ label: "15 sec.", icon: <FontAwesomeIcon icon={faClock} /> }],
modelInfo: IMAGE_MODELS.flux_pro_1_1,
},
{
label: "Flux 1 Dev",
icon: getCreatorIcon(ModelCreator.BlackForestLabs) || (
<FontAwesomeIcon icon={faImage} className="h-4 w-4" />
),
description: "High quality model",
badges: [{ label: "15 sec.", icon: <FontAwesomeIcon icon={faClock} /> }],
modelInfo: IMAGE_MODELS.flux_1_dev,
},
{
label: "Flux 1 Schnell",
icon: getCreatorIcon(ModelCreator.BlackForestLabs) || (
<FontAwesomeIcon icon={faImage} className="h-4 w-4" />
),
description: "High quality model",
badges: [{ label: "15 sec.", icon: <FontAwesomeIcon icon={faClock} /> }],
modelInfo: IMAGE_MODELS.flux_1_schnell,
},
{
label: "GPT Image 1 (GPT-4o)",
icon: getCreatorIcon(ModelCreator.OpenAi) || (
<FontAwesomeIcon icon={faImage} className="h-4 w-4" />
),
description: "Slow, ultra instructive model",
badges: [{ label: "45 sec.", icon: <FontAwesomeIcon icon={faClock} /> }],
modelInfo: IMAGE_MODELS.gpt_image_1,
},
//{
// id: "todo",
// label: "Recraft 3",
// icon: <FontAwesomeIcon icon={faImage} className="h-4 w-4" />,
// description: "Fast and high-quality model",
// badges: [{ label: "15 sec.", icon: <FontAwesomeIcon icon={faClock} /> }],
//},
//{
// id: "todo",
// label: "Flux.1 Kontext",
// icon: <FontAwesomeIcon icon={faImage} className="h-4 w-4" />,
// description: "New model with advanced context",
// badges: [{ label: "25 sec.", icon: <FontAwesomeIcon icon={faClock} /> }],
//},
],
// These models are used for 2D and 3D editing and scene building.
instructiveImageEdits: [
{
label: "GPT-4o",
icon: getCreatorIcon(ModelCreator.OpenAi) || (
<FontAwesomeIcon icon={faImage} className="h-4 w-4" />
),
description: "High quality model",
badges: [{ label: "2 min.", icon: <FontAwesomeIcon icon={faClock} /> }],
modelInfo: IMAGE_MODELS.gpt_image_1,
},
{
label: "FLUX.1 Pro Kontext Max",
icon: getCreatorIcon(ModelCreator.BlackForestLabs) || (
<FontAwesomeIcon icon={faImage} className="h-4 w-4" />
),
description: "Fast and high-quality model",
badges: [{ label: "20 sec.", icon: <FontAwesomeIcon icon={faClock} /> }],
modelInfo: IMAGE_MODELS.flux_pro_kontext_max,
},
],
video: buildItems(
videoModels as any,
<FontAwesomeIcon icon={faFilm} className="h-4 w-4" />
),
image: buildItems(
imageModels as any,
<FontAwesomeIcon icon={faImage} className="h-4 w-4" />
),
instructiveImageEdits: buildItems(
instructiveModels as any,
<FontAwesomeIcon icon={faImage} className="h-4 w-4" />
),
};
export const videoGenerationModels: ModelList = allModels.video;
@@ -22,6 +22,7 @@ import {
faUpload,
faImages,
faFrame,
faCopy,
} from "@fortawesome/pro-solid-svg-icons";
import {
faRectangleVertical,
@@ -46,7 +47,7 @@ import {
// import { invoke } from "@tauri-apps/api/core";
import { usePrompt2DStore, RefImage } from "./promptStore";
import { gtagEvent } from "@storyteller/google-analytics";
import { ModelInfo } from "@storyteller/model-list";
import { ModelInfo, getCapabilitiesForModel } from "@storyteller/model-list";
export type AspectRatio = "1:1" | "3:2" | "2:3";
@@ -92,6 +93,8 @@ export const PromptBox2D = ({
const setUseSystemPrompt = usePrompt2DStore((s) => s.setUseSystemPrompt);
const aspectRatio = usePrompt2DStore((s) => s.aspectRatio);
const setAspectRatio = usePrompt2DStore((s) => s.setAspectRatio);
const generationCount = usePrompt2DStore((s) => s.generationCount);
const setGenerationCount = usePrompt2DStore((s) => s.setGenerationCount);
const [isEnqueueing, setIsEnqueueing] = useState(false);
const [selectedGalleryImages, setSelectedGalleryImages] = useState<string[]>(
@@ -120,6 +123,41 @@ export const PromptBox2D = ({
},
]);
const [generationCountList, setGenerationCountList] = useState<PopoverItem[]>(
[]
);
// Update generation count options from selected model capabilities
useEffect(() => {
const caps = getCapabilitiesForModel(selectedModelInfo);
const items: PopoverItem[] = Array.from(
{ length: caps.maxGenerationCount },
(_, i) => i + 1
).map((count) => ({
label: String(count),
selected: count === generationCount,
}));
setGenerationCountList(items);
// Clamp selection to allowed range
if (generationCount < 1 || generationCount > caps.maxGenerationCount) {
setGenerationCount(
Math.min(Math.max(1, generationCount), caps.maxGenerationCount)
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedModelInfo]);
// Keep UI selection in sync when store value changes
useEffect(() => {
setGenerationCountList((prev) =>
prev.map((item) => ({
...item,
selected: item.label === String(generationCount),
}))
);
}, [generationCount]);
const fileInputRef = useRef<HTMLInputElement>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null);
@@ -249,6 +287,17 @@ export const PromptBox2D = ({
}
};
const handleGenerationCountSelect = (selectedItem: PopoverItem) => {
const count = parseInt(selectedItem.label, 10);
setGenerationCount(count);
setGenerationCountList((prev) =>
prev.map((item) => ({
...item,
selected: item.label === selectedItem.label,
}))
);
};
const handlePaste = (e: React.ClipboardEvent<HTMLTextAreaElement>) => {
e.preventDefault();
e.stopPropagation();
@@ -327,7 +376,7 @@ export const PromptBox2D = ({
image_media_tokens: referenceImages.map((image) => image.mediaToken),
disable_system_prompt: !useSystemPrompt,
prompt: prompt,
image_count: 1,
image_count: generationCount,
aspect_ratio: aspectRatio,
});
@@ -564,8 +613,8 @@ export const PromptBox2D = ({
onChange={handleChange}
onPaste={handlePaste}
onKeyDown={handleKeyDown}
onFocus={() => { }}
onBlur={() => { }}
onFocus={() => {}}
onBlur={() => {}}
/>
</div>
<div className="mt-2 flex items-center justify-between gap-2">
@@ -626,6 +675,23 @@ export const PromptBox2D = ({
</Tooltip>
</div>
<div className="flex items-center gap-2">
<Tooltip
content="Number of generations"
position="top"
className="z-50"
closeOnClick={true}
>
<PopoverMenu
items={generationCountList}
onSelect={handleGenerationCountSelect}
mode="toggle"
panelTitle="No. of images"
triggerIcon={
<FontAwesomeIcon icon={faCopy} className="h-4 w-4" />
}
buttonClassName="h-9"
/>
</Tooltip>
<Button
className="flex items-center border-none bg-primary px-3 text-sm text-white disabled:cursor-not-allowed disabled:opacity-50"
icon={!isEnqueueing ? faSparkles : undefined}
@@ -28,7 +28,7 @@ import {
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IconProp } from "@fortawesome/fontawesome-svg-core";
import { GalleryItem, GalleryModal } from "@storyteller/ui-gallery-modal";
import { ModelInfo } from "@storyteller/model-list";
import { ModelInfo, getCapabilitiesForModel } from "@storyteller/model-list";
import { usePromptImageStore, RefImage } from "./promptStore";
import { gtagEvent } from "@storyteller/google-analytics";
@@ -102,28 +102,7 @@ export const PromptBoxImage = ({
},
]);
const [generationCountList, setGenerationCountList] = useState<PopoverItem[]>(
[
{
label: "1",
selected: generationCount === 1,
icon: <FontAwesomeIcon icon={faCopy} className="h-4 w-4" />,
},
{
label: "2",
selected: generationCount === 2,
icon: <FontAwesomeIcon icon={faCopy} className="h-4 w-4" />,
},
{
label: "3",
selected: generationCount === 3,
icon: <FontAwesomeIcon icon={faCopy} className="h-4 w-4" />,
},
{
label: "4",
selected: generationCount === 4,
icon: <FontAwesomeIcon icon={faCopy} className="h-4 w-4" />,
},
]
[]
);
const fileInputRef = useRef<HTMLInputElement>(null);
@@ -148,6 +127,37 @@ export const PromptBoxImage = ({
}
}, [imageMediaId, url]);
useEffect(() => {
// Build generation count options based on selected model
const caps = getCapabilitiesForModel(modelInfo);
const items: PopoverItem[] = Array.from(
{ length: caps.maxGenerationCount },
(_, i) => i + 1
).map((count) => ({
label: String(count),
selected: count === generationCount,
icon: <FontAwesomeIcon icon={faCopy} className="h-4 w-4" />,
}));
setGenerationCountList(items);
// Clamp selection
if (generationCount < 1 || generationCount > caps.maxGenerationCount) {
setGenerationCount(
Math.min(Math.max(1, generationCount), caps.maxGenerationCount)
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [modelInfo]);
useEffect(() => {
setGenerationCountList((prev) =>
prev.map((item) => ({
...item,
selected: item.label === String(generationCount),
}))
);
}, [generationCount]);
useEffect(() => {
setAspectRatioList((prev) =>
prev.map((item) => ({
@@ -157,15 +167,6 @@ export const PromptBoxImage = ({
);
}, [aspectRatio]);
useEffect(() => {
setGenerationCountList((prev) =>
prev.map((item) => ({
...item,
selected: item.label === generationCount.toString(),
}))
);
}, [generationCount]);
const handleAspectRatioSelect = (selectedItem: PopoverItem) => {
setAspectRatio(selectedItem.label as any);
setAspectRatioList((prev) =>
@@ -15,10 +15,12 @@ interface Prompt2DStore {
aspectRatio: AspectRatio;
useSystemPrompt: boolean;
referenceImages: RefImage[];
generationCount: number;
setPrompt: (prompt: string) => void;
setAspectRatio: (ratio: AspectRatio) => void;
setUseSystemPrompt: (value: boolean) => void;
setReferenceImages: (images: RefImage[]) => void;
setGenerationCount: (count: number) => void;
}
export const usePrompt2DStore = create<Prompt2DStore>()((set) => ({
@@ -26,10 +28,12 @@ export const usePrompt2DStore = create<Prompt2DStore>()((set) => ({
aspectRatio: "3:2",
useSystemPrompt: true,
referenceImages: [],
generationCount: 1,
setPrompt: (prompt) => set({ prompt }),
setAspectRatio: (aspectRatio) => set({ aspectRatio }),
setUseSystemPrompt: (useSystemPrompt) => set({ useSystemPrompt }),
setReferenceImages: (referenceImages) => set({ referenceImages }),
setGenerationCount: (generationCount) => set({ generationCount }),
}));
export { usePrompt2DStore as usePromptStore };
+2 -4
View File
@@ -1,7 +1,5 @@
export * from "./lib/model-list.js";
export * from "./lib/ModelInfo.js";
export * from "./lib/ModelCreator.js";
export * from "./lib/ImageModels.js";
export * from "./lib/VideoModels.js";
export * from "./lib/ModelCreatorIcons.js";
export * from "./lib/ModelTypeMapping.js";
export * from "./lib/ModelMapping.js";
export * from "./lib/Models.js";
@@ -1,52 +0,0 @@
import { ModelCreator } from "./ModelCreator.js";
import { ModelInfo } from "./ModelInfo.js";
export const IMAGE_MODELS: Record<string, ModelInfo> = {
flux_pro_1_1_ultra: {
name: "Flux Pro 1.1 Ultra",
tauri_id: "flux_pro_11_ultra",
creator: ModelCreator.BlackForestLabs,
},
flux_pro_1_1: {
name: "Flux Pro 1.1",
tauri_id: "flux_pro_11",
creator: ModelCreator.BlackForestLabs,
},
flux_1_dev: {
name: "Flux 1 Dev",
tauri_id: "flux_1_dev",
creator: ModelCreator.BlackForestLabs,
},
flux_1_schnell: {
name: "Flux 1 Schnell",
tauri_id: "flux_1_schnell",
creator: ModelCreator.BlackForestLabs,
},
gpt_image_1: {
name: "GPT Image 1 (GPT-4o)",
tauri_id: "gpt_image_1",
creator: ModelCreator.OpenAi,
},
flux_pro_kontext_max: {
name: "Flux Pro Kontext Max",
tauri_id: "flux_pro_kontext_max",
creator: ModelCreator.BlackForestLabs,
}
};
// TODO: This needs to be cleaned up. It's gross.
// This is a hack to get the old model selector code to work since it's keyed off
// of the human-readable label of the model. Long term, that should be fixed.
export const IMAGE_MODELS_BY_LABEL: Record<string, ModelInfo> = {
// NB: This list is for text-to-image PromptBox:
"Flux Pro 1.1 Ultra": IMAGE_MODELS.flux_pro_1_1_ultra,
"Flux Pro 1.1": IMAGE_MODELS.flux_pro_1_1,
"Flux 1 Dev": IMAGE_MODELS.flux_1_dev,
"Flux 1 Schnell": IMAGE_MODELS.flux_1_schnell,
"GPT Image 1 (GPT-4o)": IMAGE_MODELS.gpt_image_1,
// NB: This is for 2D and 3D PromptBox:
"FLUX.1 Pro Kontext Max": IMAGE_MODELS.flux_pro_kontext_max,
"GPT-4o": IMAGE_MODELS.gpt_image_1,
};
@@ -8,6 +8,14 @@ export enum ModelCreator {
OpenAi = "OpenAi",
Runway = "Runway",
Stability = "Stability",
Tencent = "Tencent",
Tencent = "Tencent", // hunyuan
Recraft = "Recraft",
Krea = "Krea",
Fal = "Fal",
Replicate = "Replicate",
TensorArt = "TensorArt",
OpenArt = "OpenArt",
Higgsfield = "Higgsfield",
Alibaba = "Alibaba", // qwen and wanvideo
Vidu = "Vidu",
}
@@ -11,8 +11,18 @@ export const CREATOR_ICON_PATHS: Partial<Record<ModelCreator, string>> = {
[ModelCreator.Google]: "/resources/images/services/google.svg",
[ModelCreator.Recraft]: "/resources/images/services/recraft.svg",
[ModelCreator.Tencent]: "/resources/images/services/tencent.svg",
[ModelCreator.Krea]: "/resources/images/services/krea.svg",
[ModelCreator.Fal]: "/resources/images/services/fal.svg",
[ModelCreator.Replicate]: "/resources/images/services/replicate.svg",
[ModelCreator.TensorArt]: "/resources/images/services/tensorart.svg",
[ModelCreator.OpenArt]: "/resources/images/services/openart.svg",
[ModelCreator.Higgsfield]: "/resources/images/services/higgsfield.svg",
[ModelCreator.Alibaba]: "/resources/images/services/alibaba.svg",
[ModelCreator.Vidu]: "/resources/images/services/vidu.svg",
};
const DEFAULT_ICON_PATH = "/resources/images/services/generic.svg";
export const getCreatorIconPath = (creator: ModelCreator): string | undefined =>
CREATOR_ICON_PATHS[creator];
@@ -20,8 +30,7 @@ export const getCreatorIcon = (
creator: ModelCreator,
className = "h-4 w-4 invert"
): ReactNode | null => {
const path = getCreatorIconPath(creator);
if (!path) return null;
const path = getCreatorIconPath(creator) ?? DEFAULT_ICON_PATH;
return React.createElement("img", {
src: path,
alt: `${creator} logo`,
@@ -55,6 +55,22 @@ export const getModelCreatorName = (modelType: string): string | null => {
return "Recraft";
case ModelCreator.Tencent:
return "Tencent";
case ModelCreator.Alibaba:
return "Alibaba";
case ModelCreator.Vidu:
return "Vidu";
case ModelCreator.Fal:
return "Fal";
case ModelCreator.Replicate:
return "Replicate";
case ModelCreator.TensorArt:
return "TensorArt";
case ModelCreator.OpenArt:
return "OpenArt";
case ModelCreator.Higgsfield:
return "Higgsfield";
case ModelCreator.Krea:
return "Krea";
default:
return creator;
}
@@ -77,12 +93,14 @@ export const getModelDisplayName = (modelType: string): string => {
hunyuan_3d_2p0: "Hunyuan 3D 2.0",
hunyuan_3d_2p1: "Hunyuan 3D 2.1",
hunyuan_3d: "Hunyuan 3D",
flux_pro_kontext_max: "Flux Pro Kontext Max",
// TODO: add more models here - BFlat
};
return displayNames[modelType] || modelType;
};
// Convert provider string to human-readable display name
// Convert provider string to human-readable display name (this is for the provider priority in settings)
export const getProviderDisplayName = (provider: string): string => {
const displayNames: Record<string, string> = {
artcraft: "ArtCraft",
+209
View File
@@ -0,0 +1,209 @@
import { ModelCreator } from "./ModelCreator.js";
import { ModelInfo } from "./ModelInfo.js";
export type ModelCategory = "image" | "video";
export interface ModelConfig {
id: string;
label: string; // UI label
description?: string;
badges?: { label: string }[];
category: ModelCategory;
info: ModelInfo;
capabilities: ModelCapabilities;
tags?: string[]; // optional tags, e.g. ["instructiveEdit"] - for filtering
}
const mc = ModelCreator;
// Centralized model capability definition
// For setting options for the model
// TODO: add more capabilities here - BFlat
export interface ModelCapabilities {
maxGenerationCount: number;
}
const DEFAULT_CAPABILITIES: ModelCapabilities = {
maxGenerationCount: 1,
};
const cfg = (
m: Partial<ModelConfig> & {
id: string;
category: ModelCategory;
info: ModelInfo;
}
): ModelConfig => ({
label: m.label ?? m.info.name,
description: m.description,
badges: m.badges,
capabilities: m.capabilities ?? DEFAULT_CAPABILITIES,
tags: m.tags ?? [],
...m,
});
export const ALL_MODELS: ModelConfig[] = [
//////////////////////////////
// Image models
//////////////////////////////
cfg({
id: "flux_pro_1_1_ultra",
category: "image",
info: {
name: "Flux Pro 1.1 Ultra",
tauri_id: "flux_pro_11_ultra",
creator: mc.BlackForestLabs,
},
description: "High quality model",
badges: [{ label: "15 sec." }],
capabilities: { maxGenerationCount: 4 },
tags: ["instructiveEdit"],
}),
cfg({
id: "flux_pro_1_1",
category: "image",
info: {
name: "Flux Pro 1.1",
tauri_id: "flux_pro_11",
creator: mc.BlackForestLabs,
},
description: "High quality model",
badges: [{ label: "15 sec." }],
capabilities: { maxGenerationCount: 4 },
tags: ["instructiveEdit"],
}),
cfg({
id: "flux_1_dev",
category: "image",
info: {
name: "Flux 1 Dev",
tauri_id: "flux_1_dev",
creator: mc.BlackForestLabs,
},
description: "High quality model",
badges: [{ label: "15 sec." }],
capabilities: { maxGenerationCount: 4 },
}),
cfg({
id: "flux_1_schnell",
category: "image",
info: {
name: "Flux 1 Schnell",
tauri_id: "flux_1_schnell",
creator: mc.BlackForestLabs,
},
description: "High quality model",
badges: [{ label: "15 sec." }],
capabilities: { maxGenerationCount: 4 },
}),
cfg({
id: "gpt_image_1",
category: "image",
info: {
name: "GPT Image 1 (GPT-4o)",
tauri_id: "gpt_image_1",
creator: mc.OpenAi,
},
description: "Slow, ultra instructive model",
badges: [{ label: "45 sec." }],
capabilities: { maxGenerationCount: 1 },
tags: ["instructiveEdit"],
}),
cfg({
id: "flux_pro_kontext_max",
category: "image",
info: {
name: "Flux Pro Kontext Max",
tauri_id: "flux_pro_kontext_max",
creator: mc.BlackForestLabs,
},
description: "Fast and high-quality model",
badges: [{ label: "20 sec." }],
capabilities: { maxGenerationCount: 4 },
tags: ["instructiveEdit"],
}),
//////////////////////////////
// Video models
//////////////////////////////
cfg({
id: "kling_1_6_pro",
category: "video",
info: {
name: "Kling 1.6 Pro",
tauri_id: "kling_1.6_pro",
creator: mc.Kling,
},
description: "Good quality model",
badges: [{ label: "2 min." }],
capabilities: { maxGenerationCount: 1 },
}),
cfg({
id: "kling_2_1_pro",
category: "video",
info: {
name: "Kling 2.1 Pro",
tauri_id: "kling_2.1_pro",
creator: mc.Kling,
},
description: "High quality model",
badges: [{ label: "2 min." }],
capabilities: { maxGenerationCount: 1 },
}),
cfg({
id: "kling_2_1_master",
category: "video",
info: {
name: "Kling 2.1 Master",
tauri_id: "kling_2.1_master",
creator: mc.Kling,
},
description: "Master quality model ($$)",
badges: [{ label: "2 min." }],
capabilities: { maxGenerationCount: 1 },
}),
cfg({
id: "seedance_1_0_lite",
category: "video",
info: {
name: "Seedance 1.0 Lite",
tauri_id: "seedance_1.0_lite",
creator: mc.Bytedance,
},
description: "Fast and high-quality model",
badges: [{ label: "2 min." }],
capabilities: { maxGenerationCount: 1 },
}),
cfg({
id: "veo_2",
category: "video",
info: { name: "Google Veo 2", tauri_id: "veo_2", creator: mc.Google },
description: "Fast and high-quality model",
badges: [{ label: "2 min." }],
capabilities: { maxGenerationCount: 1 },
}),
];
export const getAllModels = (): ModelConfig[] => ALL_MODELS;
export const getModelsByCategory = (category: ModelCategory): ModelConfig[] =>
ALL_MODELS.filter((m) => m.category === category);
export const getInstructiveImageEditModels = (): ModelConfig[] =>
ALL_MODELS.filter(
(m) => m.category === "image" && m.tags?.includes("instructiveEdit")
);
export const lookupModelByTauriId = (
tauriId: string
): ModelConfig | undefined =>
ALL_MODELS.find((m) => m.info.tauri_id === tauriId);
// Single exported capability resolver so callers never need to touch anything else
export const getCapabilitiesForModel = (
model?: ModelInfo
): ModelCapabilities => {
if (!model) return DEFAULT_CAPABILITIES;
const cfg = lookupModelByTauriId(model.tauri_id);
return cfg?.capabilities ?? DEFAULT_CAPABILITIES;
};
@@ -1,40 +0,0 @@
import { ModelCreator } from "./ModelCreator.js";
import { ModelInfo } from "./ModelInfo.js";
export const VIDEO_MODELS: Record<string, ModelInfo> = {
kling_1_6_pro: {
name: "Kling 1.6 Pro",
tauri_id: "kling_1.6_pro",
creator: ModelCreator.Kling,
},
kling_2_1_pro: {
name: "Kling 2.1 Pro",
tauri_id: "kling_2.1_pro",
creator: ModelCreator.Kling,
},
kling_2_1_master: {
name: "Kling 2.1 Master",
tauri_id: "kling_2.1_master",
creator: ModelCreator.Kling,
},
seedance_1_0_lite: {
name: "Seedance 1.0 Lite",
tauri_id: "seedance_1.0_lite",
creator: ModelCreator.Bytedance,
},
veo_2: {
name: "Google Veo 2",
tauri_id: "veo_2",
creator: ModelCreator.Google,
},
};
// This is a hack to get the old model selector code to work since it's keyed off
// of the human-readable label of the model. Long term, that should be fixed.
export const VIDEO_MODELS_BY_LABEL: Record<string, ModelInfo> = {
"Kling 1.6 Pro": VIDEO_MODELS.kling_1_6_pro,
"Kling 2.1 Pro": VIDEO_MODELS.kling_2_1_pro,
"Kling 2.1 Master": VIDEO_MODELS.kling_2_1_master,
"Seedance 1.0 Lite": VIDEO_MODELS.seedance_1_0_lite,
"Google Veo 2": VIDEO_MODELS.veo_2,
};
@@ -1,7 +0,0 @@
import { modelList } from './model-list.js';
describe('modelList', () => {
it('should work', () => {
expect(modelList()).toEqual('model-list');
})
})
@@ -1,3 +0,0 @@
export function modelList(): string {
return "model-list";
}