Compare commits

...
48 changed files with 797 additions and 326 deletions
@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO tasks (\n id,\n task_status,\n task_type,\n provider,\n provider_job_id,\n frontend_subscriber_id,\n frontend_subscriber_payload\n )\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 7
},
"nullable": []
},
"hash": "0848cd9ddf8f61f2dd4d57f0c58b66a60a985acaa1ee5753f30f5e6e107e080e"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO tasks (\n id,\n task_status,\n task_type,\n model_type,\n provider,\n provider_job_id,\n frontend_subscriber_id,\n frontend_subscriber_payload\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 8
},
"nullable": []
},
"hash": "22b91a21cbb88abc9bc17b338cf2f9418e08e414667d44476623eb141ab17b87"
}
@@ -1,4 +1,7 @@
-- noinspection SqlDialectInspectionForFile
-- Migration history:
-- tasks_v1.sqlite - initial version
-- tasks_v2.sqlite - added model_type (nullable)
CREATE TABLE tasks (
id TEXT NOT NULL PRIMARY KEY,
@@ -6,6 +9,8 @@ CREATE TABLE tasks (
task_status TEXT NOT NULL,
task_type TEXT NOT NULL,
model_type TEXT,
provider TEXT,
provider_job_id TEXT,
@@ -1,6 +1,7 @@
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::core::state::task_database::TaskDatabase;
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_model_type::TaskModelType;
use enums::tauri::tasks::task_status::TaskStatus;
use enums::tauri::tasks::task_type::TaskType;
use sqlite_tasks::error::SqliteTasksError;
@@ -34,10 +35,33 @@ impl TaskEnqueueSuccess{
}
pub async fn insert_into_task_database(&self, task_database: &TaskDatabase) -> Result<TaskId, SqliteTasksError> {
// TODO: Move this mapping elsewhere, or remove the other models.
let model_type = match self.model {
None => None,
Some(GenerationModel::Flux1Dev) => Some(TaskModelType::Flux1Dev),
Some(GenerationModel::Flux1Schnell) => Some(TaskModelType::Flux1Schnell),
Some(GenerationModel::FluxPro11) => Some(TaskModelType::FluxPro11),
Some(GenerationModel::FluxPro11Ultra) => Some(TaskModelType::FluxPro11Ultra),
Some(GenerationModel::GptImage1) => Some(TaskModelType::GptImage1),
Some(GenerationModel::Recraft3) => Some(TaskModelType::Recraft3),
Some(GenerationModel::Kling21Pro) => Some(TaskModelType::Kling21Pro),
Some(GenerationModel::Kling21Master) => Some(TaskModelType::Kling21Master),
Some(GenerationModel::Seedance10Lite) => Some(TaskModelType::Seedance10Lite),
Some(GenerationModel::Veo2) => Some(TaskModelType::Veo2),
Some(GenerationModel::Hunyuan3d2_0) => Some(TaskModelType::Hunyuan3d2_0),
Some(GenerationModel::Hunyuan3d2_1) => Some(TaskModelType::Hunyuan3d2_1),
// TODO: These seem wrong -
Some(GenerationModel::Kling1_6) => Some(TaskModelType::Kling16Pro), // NB: `VideoModel::Kling16Pro`.
Some(GenerationModel::Kling2_0) => None, // TODO: unused elsewhere?
Some(GenerationModel::Sora) => None, // TODO: unused elsewhere?
};
create_task(CreateTaskArgs {
db: task_database.get_connection(),
status: TaskStatus::Pending,
task_type: self.task_type,
model_type: model_type,
provider: self.provider,
provider_job_id: self.provider_job_id.as_deref(),
frontend_subscriber_id: None,
@@ -25,8 +25,11 @@ pub enum GenerationModel {
Hunyuan3d2_0,
#[serde(rename = "hunyuan_3d_2_1")]
Hunyuan3d2_1,
// TODO: Should be Kling16Pro
#[serde(rename = "kling_1.6")]
Kling1_6,
#[serde(rename = "kling_2.0")]
Kling2_0,
#[serde(rename = "kling_2.1_master")]
@@ -1,7 +1,18 @@
use crate::core::lifecycle::startup::bootstrap_task_database::bootstrap_task_database;
use crate::core::lifecycle::startup::spawn_storytoller_task_polling_thread::spawn_storyteller_task_polling_thread;
use crate::core::lifecycle::startup::tasks::bootstrap_task_database::bootstrap_task_database;
use crate::core::lifecycle::startup::tasks::load_provider_priority_state::load_provider_priority_state;
use crate::core::lifecycle::startup::tasks::set_app_log_level::set_app_log_level;
use crate::core::lifecycle::startup::tasks::initially_size_and_position_windows::initially_size_and_position_windows;
use crate::core::lifecycle::startup::tasks::spawn_discord_presence_thread::spawn_discord_presence_thread;
use crate::core::lifecycle::startup::tasks::spawn_fal_task_polling_thread::spawn_fal_task_polling_thread;
use crate::core::lifecycle::startup::tasks::spawn_main_window_thread::spawn_main_window_thread;
use crate::core::lifecycle::startup::tasks::spawn_sora_task_polling_thread::spawn_sora_task_polling_thread;
use crate::core::lifecycle::startup::tasks::spawn_storytoller_task_polling_thread::spawn_storyteller_task_polling_thread;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use errors::AnyhowResult;
use tauri::{AppHandle, Manager};
@@ -11,8 +22,30 @@ pub async fn handle_tauri_startup(
root: AppDataRoot,
app_env_configs: AppEnvConfigs,
storyteller_creds_manager: StorytellerCredentialManager,
sora_credential_manager: SoraCredentialManager,
sora_task_queue: SoraTaskQueue,
fal_credential_manager: FalCredentialManager,
fal_task_queue: FalTaskQueue,
) -> AnyhowResult<()> {
let task_database = bootstrap_task_database(&app, &root).await?;
set_app_log_level(
&app,
&root,
)?;
let task_database =
bootstrap_task_database(&app, &root).await?;
load_provider_priority_state(
&app,
&root,
)?;
spawn_main_window_thread(
&app,
&root,
&storyteller_creds_manager,
)?;
spawn_storyteller_task_polling_thread(
&app,
@@ -21,5 +54,27 @@ pub async fn handle_tauri_startup(
&storyteller_creds_manager,
)?;
spawn_sora_task_polling_thread(
&app,
&root,
&app_env_configs,
&sora_credential_manager,
&storyteller_creds_manager,
&sora_task_queue,
)?;
spawn_fal_task_polling_thread(
&app,
&root,
&app_env_configs,
&fal_credential_manager,
&fal_task_queue,
&storyteller_creds_manager,
)?;
spawn_discord_presence_thread()?;
initially_size_and_position_windows(&app, &root);
Ok(())
}
@@ -1,3 +1,2 @@
pub mod bootstrap_task_database;
pub mod handle_tauri_startup;
mod spawn_storytoller_task_polling_thread;
mod tasks;
@@ -0,0 +1,42 @@
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::window::main_window_position::MainWindowPosition;
use crate::core::state::window::main_window_size::MainWindowSize;
use crate::core::utils::window::position_main_window::position_main_window;
use crate::core::utils::window::resize_main_window::resize_main_window;
use tauri::AppHandle;
pub fn initially_size_and_position_windows(
app: &AppHandle,
root: &AppDataRoot,
) {
println!("Sizing and positioning window...");
match MainWindowSize::from_filesystem_configs(&root) {
Ok(None) => {}
Ok(Some(size)) => {
println!("Resizing window to: {:?}", size);
let result = resize_main_window(app, &size);
if let Err(err) = result {
eprintln!("Could not set window size: {:?}", err);
}
}
Err(err) => {
eprintln!("Failed to read window size from disk: {:?}", err);
}
}
match MainWindowPosition::from_filesystem_configs(&root) {
Ok(None) => {}
Ok(Some(pos)) => {
println!("Moving window to: {:?}", pos);
let result = position_main_window(app, &pos);
if let Err(err) = result {
eprintln!("Could not set window position: {:?}", err);
}
}
Err(err) => {
eprintln!("Failed to read window position from disk: {:?}", err);
}
}
}
@@ -0,0 +1,34 @@
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::provider_priority::ProviderPriorityStore;
use anyhow::anyhow;
use errors::AnyhowResult;
use tauri::{AppHandle, Manager};
pub fn load_provider_priority_state(
app: &AppHandle,
root: &AppDataRoot,
) -> AnyhowResult<()> {
let provider_priority = match ProviderPriorityStore::from_filesystem_configs(root) {
Ok(Some(priority)) => {
println!("Loaded provider priority from disk: {:?}", priority.get_priority());
priority
}
Ok(None) => {
println!("No provider priority found on disk, using default.");
ProviderPriorityStore::default()
}
Err(err) => {
eprintln!("Failed to read provider priority from disk: {:?}", err);
ProviderPriorityStore::default()
}
};
let success = app.manage(provider_priority);
if !success {
eprintln!("Failed to manage provider priority state.");
return Err(anyhow!("Failed to manage provider priority state."));
}
Ok(())
}
@@ -0,0 +1,9 @@
pub (super) mod bootstrap_task_database;
pub (super) mod set_app_log_level;
pub (super) mod initially_size_and_position_windows;
pub (super) mod spawn_discord_presence_thread;
pub (super) mod spawn_fal_task_polling_thread;
pub (super) mod spawn_main_window_thread;
pub (super) mod spawn_sora_task_polling_thread;
pub (super) mod spawn_storytoller_task_polling_thread;
pub (super) mod load_provider_priority_state;
@@ -0,0 +1,73 @@
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use errors::AnyhowResult;
use log::LevelFilter;
use std::env;
use std::env::VarError;
use tauri::AppHandle;
use tauri_plugin_log::{Target, TargetKind};
const RUST_LOG: &str = "RUST_LOG";
pub fn set_app_log_level(
app: &AppHandle,
root: &AppDataRoot,
) -> AnyhowResult<()> {
//print_env_var(RUST_LOG);
// NB: The semantics of this do not match `RUST_LOG` exactly as it's not set per-package.
let maybe_level = env::var(RUST_LOG)
.map(|value| value.trim().to_ascii_lowercase());
let mut log_level = LevelFilter::Info;
match maybe_level {
Ok(level) => {
println!("The `RUST_LOG` environment variable was set to: `{}`.", level);
if level.contains("debug") {
log_level = LevelFilter::Debug;
} else if level.contains("info") {
log_level = LevelFilter::Info;
} else if level.contains("warn") {
log_level = LevelFilter::Warn;
} else if level.contains("error") {
log_level = LevelFilter::Error;
} else if level.contains("off") {
log_level = LevelFilter::Off;
}
}
Err(VarError::NotPresent) => {
// NB: This demonstrably tests stderr output in the default case.
eprintln!("No `RUST_LOG` environment variable found.");
}
Err(VarError::NotUnicode(_)) => {
// NB: This demonstrably tests stderr output in the default case.
eprintln!("Could not parse `RUST_LOG` environment variable; not unicode.");
}
}
println!("Setting app log level to: {:?}", log_level);
app.plugin(
tauri_plugin_log::Builder::default()
.level(log_level)
.targets(vec![
Target::new(TargetKind::Stdout),
Target::new(TargetKind::LogDir {
file_name: Some(root.log_file_name_str().to_string())
}),
])
.build(),
)?;
Ok(())
}
// pub fn print_env_var(var_name: &str) {
// match env::var(var_name) {
// Ok(value) => println!("Environment variable `{}` = {}", var_name, value),
// Err(VarError::NotPresent) => println!("Environment variable `{}` is not set.", var_name),
// Err(VarError::NotUnicode(_)) => println!("Environment variable `{}` is not a valid Unicode string", var_name),
// }
// }
@@ -0,0 +1,10 @@
use crate::core::threads::discord_presence_thread::discord_presence_thread;
use errors::AnyhowResult;
pub fn spawn_discord_presence_thread(
) -> AnyhowResult<()> {
tauri::async_runtime::spawn(discord_presence_thread());
Ok(())
}
@@ -0,0 +1,29 @@
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
use crate::services::fal::threads::fal_task_polling_thread::fal_task_polling_thread;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use errors::AnyhowResult;
use tauri::{AppHandle, Manager};
pub fn spawn_fal_task_polling_thread(
app: &AppHandle,
root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
fal_credential_manager: &FalCredentialManager,
fal_task_queue: &FalTaskQueue,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> AnyhowResult<()> {
tauri::async_runtime::spawn(fal_task_polling_thread(
app.clone(),
app_env_configs.clone(),
root.clone(),
fal_credential_manager.clone(),
fal_task_queue.clone(),
storyteller_creds_manager.clone(),
));
Ok(())
}
@@ -0,0 +1,20 @@
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::threads::main_window_thread::main_window_thread::main_window_thread;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use errors::AnyhowResult;
use tauri::{AppHandle, Manager};
pub fn spawn_main_window_thread(
app: &AppHandle,
root: &AppDataRoot,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> AnyhowResult<()> {
tauri::async_runtime::spawn(main_window_thread(
app.clone(),
root.clone(),
storyteller_creds_manager.clone(),
));
Ok(())
}
@@ -0,0 +1,29 @@
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use crate::services::sora::threads::sora_task_polling_thread::sora_task_polling_thread;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use errors::AnyhowResult;
use tauri::{AppHandle, Manager};
pub fn spawn_sora_task_polling_thread(
app: &AppHandle,
root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
sora_credential_manager: &SoraCredentialManager,
storyteller_creds_manager: &StorytellerCredentialManager,
sora_task_queue: &SoraTaskQueue,
) -> AnyhowResult<()> {
tauri::async_runtime::spawn(sora_task_polling_thread(
app.clone(),
app_env_configs.clone(),
root.clone(),
sora_credential_manager.clone(),
storyteller_creds_manager.clone(),
sora_task_queue.clone(),
));
Ok(())
}
@@ -22,7 +22,7 @@ impl DataSubdir for AppStateDir{
impl AppStateDir {
pub fn get_tasks_sqlite_database_path(&self) -> PathBuf {
self.path.join("tasks_v1.sqlite")
self.path.join("tasks_v2.sqlite")
}
pub fn get_window_size_config_file(&self) -> PathBuf {
@@ -3,8 +3,7 @@ pub mod app_preferences;
pub mod app_startup_time;
pub mod data_dir;
pub mod expanduser;
pub mod main_window_position;
pub mod main_window_size;
pub mod os_platform;
pub mod provider_priority;
pub mod task_database;
pub mod task_database;
pub mod window;
@@ -42,14 +42,6 @@ impl MainWindowPosition {
Ok(Some(pos))
}
pub fn apply_to_main_window(&self, app: &AppHandle) -> AnyhowResult<()> {
let windows = app.windows();
let window = windows.get(MAIN_WINDOW_NAME)
.ok_or_else(|| anyhow::anyhow!("Main window not found"))?;
window.set_position(PhysicalPosition::new(self.x, self.y))?;
Ok(())
}
pub fn persist_to_filesystem(&self, app_data_root: &AppDataRoot) -> AnyhowResult<()> {
let filename = app_data_root.get_window_position_config_file();
let json = serde_json::to_string(self)?;
@@ -1,12 +1,13 @@
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::utils::best_window_size_heuristic::best_window_size_heuristic;
use crate::core::windows::main_window::constants::MAIN_WINDOW_NAME;
use crate::core::utils::window::get_window_size_heuristic::get_window_size_heuristic;
use crate::core::windows::main_window::constants::{MAIN_WINDOW_MIN_HEIGHT, MAIN_WINDOW_MIN_WIDTH, MAIN_WINDOW_NAME};
use errors::AnyhowResult;
use serde_derive::{Deserialize, Serialize};
use std::fs::OpenOptions;
use std::io::Write;
use tauri::{AppHandle, Manager, PhysicalSize, Window};
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
pub struct MainWindowSize {
pub width: u32,
@@ -26,7 +27,7 @@ impl MainWindowSize {
}
pub fn from_window(window: &Window) -> AnyhowResult<Self> {
let size = best_window_size_heuristic(window)?;
let size = get_window_size_heuristic(window)?;
Ok(Self {
width: size.width,
height: size.height,
@@ -43,14 +44,6 @@ impl MainWindowSize {
Ok(Some(size))
}
pub fn apply_to_main_window(&self, app: &AppHandle) -> AnyhowResult<()> {
let windows = app.windows();
let window = windows.get(MAIN_WINDOW_NAME)
.ok_or_else(|| anyhow::anyhow!("Main window not found"))?;
window.set_size(PhysicalSize::new(self.width, self.height))?;
Ok(())
}
pub fn persist_to_filesystem(&self, app_data_root: &AppDataRoot) -> AnyhowResult<()> {
let filename = app_data_root.get_window_size_config_file();
let json = serde_json::to_string(self)?;
@@ -64,6 +57,21 @@ impl MainWindowSize {
Ok(())
}
/// Window size is not wide enough to persist or apply.
pub fn is_not_wide_enough(&self) -> bool {
self.width < MAIN_WINDOW_MIN_WIDTH
}
/// Window size is not tall enough to persist or apply.
pub fn is_not_tall_enough(&self) -> bool {
self.height < MAIN_WINDOW_MIN_HEIGHT
}
/// Window size is not big enough (tall or wide) to persist or apply.
pub fn is_not_big_enough(&self) -> bool {
self.is_not_wide_enough() || self.is_not_tall_enough()
}
pub fn to_physical_size(&self) -> PhysicalSize<u32> {
PhysicalSize::new(self.width, self.height)
}
@@ -0,0 +1,2 @@
pub mod main_window_position;
pub mod main_window_size;
@@ -1,10 +0,0 @@
pub mod frontend_subscriber_id;
pub mod frontend_subscriber_payload;
pub mod task_provider;
pub mod task;
pub mod task_id;
pub mod task_pool;
pub mod task_type;
mod task_provider_job_id;
mod task_state;
pub mod task_database;
@@ -1,42 +0,0 @@
use multi_index_map::MultiIndexMap;
use crate::core::tasks::task_id::TaskId;
use crate::core::tasks::task_provider::TaskProvider;
use crate::core::tasks::task_provider_job_id::TaskProviderJobId;
use crate::core::tasks::task_state::TaskState;
use crate::core::tasks::task_type::TaskType;
/// The provider's native identifier for the task.
pub type ProviderTaskId = String;
#[derive(MultiIndexMap, Debug, Clone)]
#[multi_index_derive(Debug)]
#[multi_index_hash(rustc_hash::FxBuildHasher)]
pub struct Task {
#[multi_index(hashed_unique)]
pub id: TaskId,
#[multi_index(hashed_non_unique)]
pub task_state: TaskState,
pub task_type: Option<TaskType>,
#[multi_index(hashed_non_unique)]
pub provider: Option<TaskProvider>,
/// Identifier used by the provider to track the job.
#[multi_index(hashed_unique)]
pub provider_job_id: Option<TaskProviderJobId>,
/// If the frontend is subscribing, it might have an identifier
pub frontend_subscriber_id: Option<String>,
/// If the frontend is subscribing, it might have a payload.
/// This is opaque, but could be for instance base64-encoded JSON.
pub frontend_subscriber_payload: Option<String>,
// TODO: Submitted and completion datetimes
// TODO: Progress
// TODO: Completion media file, completion batch token
}
@@ -1,21 +0,0 @@
pub struct TaskDatabase {
pub pool: Pool<Sqlite>,
}
impl Database {
pub async fn new(app_handle: &AppHandle) -> Result<Self> {
let app_dir = app_handle
.path_resolver()
.app_data_dir()
.expect("failed to get app dir");
// Ensure the app directory exists
fs::create_dir_all(&app_dir)?;
let db_path = app_dir.join("grid_search.db");
// Set the DATABASE_URL environment variable to point to this SQLite file
env::set_var("DATABASE_URL", format!("sqlite://{}", db_path.display()));
}
}
@@ -1,25 +0,0 @@
use idempotency::uuid::generate_random_uuid;
/// Our identifier for tasks.
/// These are ephemeral (for now), but may be peristed later on.
#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct TaskId(String);
impl TaskId {
pub fn generate() -> Self {
let task_id = format!("tauri_task_{}", generate_random_uuid());
Self(task_id)
}
pub fn from_str(task_id: &str) -> Self {
Self(task_id.to_string())
}
pub fn to_string(&self) -> String {
self.0.clone()
}
pub fn as_str(&self) -> &str {
self.0.as_str()
}
}
@@ -1,7 +0,0 @@
/// Hand-rolling this instead of SQLite for now.
/// Not sure if SQLite will create crazy build issues.
/// Can replace with SQLite later, perhaps.
pub struct TaskPool {
}
@@ -1,8 +0,0 @@
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub enum TaskProvider {
Artcraft,
Fal,
Sora,
}
@@ -1,22 +0,0 @@
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct TaskProviderJobId(String);
impl TaskProviderJobId {
pub fn from_str(job_id: &str) -> Self {
Self(job_id.to_string())
}
pub fn from_artcraft_job_id(job_id: InferenceJobToken) -> Self {
Self(job_id.to_string())
}
pub fn to_string(&self) -> String {
self.0.clone()
}
pub fn as_str(&self) -> &str {
self.0.as_str()
}
}
@@ -1,11 +0,0 @@
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub enum TaskState {
Submitted,
InProgress,
// Terminal states
Complete,
Failed,
}
@@ -1,7 +0,0 @@
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub enum TaskType {
BackgroundRemoval,
ImageGeneration,
VideoGeneration,
}
@@ -1,7 +1,7 @@
use crate::core::state::app_startup_time::AppStartupTime;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::main_window_position::MainWindowPosition;
use crate::core::state::main_window_size::MainWindowSize;
use crate::core::state::window::main_window_position::MainWindowPosition;
use crate::core::state::window::main_window_size::MainWindowSize;
use crate::core::threads::main_window_thread::persist_storyteller_cookies_task::persist_storyteller_cookies_task;
use crate::core::threads::main_window_thread::persist_window_position_task::persist_window_position_task;
use crate::core::threads::main_window_thread::persist_window_resize_task::persist_window_resize_task;
@@ -1,5 +1,5 @@
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::main_window_position::MainWindowPosition;
use crate::core::state::window::main_window_position::MainWindowPosition;
use errors::AnyhowResult;
use log::info;
use memory_store::clone_slot::CloneSlot;
@@ -1,6 +1,6 @@
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::main_window_size::MainWindowSize;
use crate::core::utils::best_window_size_heuristic::best_window_size_heuristic;
use crate::core::state::window::main_window_size::MainWindowSize;
use crate::core::utils::window::get_window_size_heuristic::get_window_size_heuristic;
use errors::AnyhowResult;
use log::info;
use memory_store::clone_slot::CloneSlot;
@@ -13,7 +13,7 @@ pub async fn persist_window_resize_task(
) -> AnyhowResult<()> {
let current_window_size = MainWindowSize::from_window(window)?;
let current_physical_size = best_window_size_heuristic(window)?;
let current_physical_size = get_window_size_heuristic(window)?;
let mut save_size_to_disk = true;
@@ -22,6 +22,10 @@ pub async fn persist_window_resize_task(
save_size_to_disk = false;
}
}
if current_window_size.is_not_big_enough() {
save_size_to_disk = false;
}
if save_size_to_disk {
info!("Saving window size configs to disk...");
@@ -1,4 +1,3 @@
pub mod best_window_size_heuristic;
pub mod clear_all_webview_cookies;
pub mod download_media_file_to_temp_dir;
pub mod download_url_to_temp_dir;
@@ -10,4 +9,4 @@ pub mod object_store;
pub mod save_base64_image_to_temp_dir;
pub mod simple_http_download;
pub mod simple_http_download_to_tempfile;
pub mod webview_unsafe;
pub mod window;
@@ -1,38 +0,0 @@
use errors::AnyhowResult;
use tauri::{AppHandle, Manager};
const MAIN_WINDOW_NAME: &str = "main";
pub fn webview_unsafe_for_app(app: &AppHandle) -> AnyhowResult<()> {
let windows = app.windows();
let maybe_window = windows.get(MAIN_WINDOW_NAME);
/*if let Some(window) = maybe_window {
let webview = window.get_webview(MAIN_WINDOW_NAME);
if let Some(webview) = webview {
webview_unsafe(&webview);
}
} else {
return Err(anyhow::anyhow!("Main window not found"));
}*/
Ok(())
}
/*pub fn webview_unsafe(webview: &Webview) {
let r = webview.with_webview(|webview| {
webview.controller();
#[cfg(target_os = "macos")]
unsafe {
let view: &objc2_web_kit::WKWebView = &*webview.inner().cast();
let controller: &objc2_web_kit::WKUserContentController = &*webview.controller().cast();
let window: &objc2_app_kit::NSWindow = &*webview.ns_window().cast();
//view.setPageZoom(40.);
//controller.removeAllUserScripts();
let bg_color = objc2_app_kit::NSColor::colorWithDeviceRed_green_blue_alpha(1.0, 0.2, 0.4, 1.);
window.setBackgroundColor(Some(&bg_color));
}
});
}*/
@@ -4,18 +4,18 @@ use tauri::{PhysicalSize, Window};
// A typical height difference when dev tools are open:
// Window inner size = PhysicalSize { width: 3704, height: 732 }
// Window outer size = PhysicalSize { width: 3704, height: 1476 }
const HEIGHT_DIFFERENCE_THRESHOLD: u32 = 300;
const DEV_TOOLS_HEIGHT_DIFFERENCE_THRESHOLD: u32 = 300;
/// Get the best window size
pub fn best_window_size_heuristic(window: &Window) -> AnyhowResult<PhysicalSize<u32>> {
pub fn get_window_size_heuristic(window: &Window) -> AnyhowResult<PhysicalSize<u32>> {
// NB: Ordinarily we'd use inner size, as the outer size includes the window decorations.
// However, dev tools contract the inner size, and in the event they're open, we should
// prefer the outer size.
let inner = window.inner_size()?;
let outer = window.outer_size()?;
let height_delta = outer.height.saturating_sub(inner.height);
if height_delta > HEIGHT_DIFFERENCE_THRESHOLD {
if height_delta > DEV_TOOLS_HEIGHT_DIFFERENCE_THRESHOLD {
// Dev tools are open, use the outer size
Ok(outer)
} else {
@@ -0,0 +1,3 @@
pub mod get_window_size_heuristic;
pub mod position_main_window;
pub mod resize_main_window;
@@ -0,0 +1,65 @@
use crate::core::state::window::main_window_position::MainWindowPosition;
use crate::core::windows::main_window::constants::MAIN_WINDOW_NAME;
use errors::AnyhowResult;
use tauri::{AppHandle, Manager, PhysicalPosition};
/// Try to position the main window at startup.
/// If it doesn't meet certain placement heuristics, abandon manual placement and let the OS
/// decide where to place the app.
pub fn position_main_window(
app: &AppHandle,
position: &MainWindowPosition,
) -> AnyhowResult<()> {
// NB: This non-negative position heuristic is good for MacBook (single screen).
// Not sure how this fares on Windows, Linux, or multiscreen setups.
// On Mac, this prevents the windows from being positioned off to the top-left,
// as (0,0) is the top left directly under the top menu bar.
if position.x < 0 {
// TODO(bt,2025-07-16): just in the case of x/width, allow slight placement off
// screen as long as the app is wide enough.
return Err(anyhow::anyhow!("X position must be non-negative"));
} else if position.y < 0 {
return Err(anyhow::anyhow!("Y position must be non-negative"));
}
let windows = app.windows();
let window = windows.get(MAIN_WINDOW_NAME)
.ok_or_else(|| anyhow::anyhow!("Main window not found"))?;
if let Ok(Some(monitor)) = window.current_monitor() {
// Reports the size of the monitor, e.g. PhysicalSize { width: 4112, height: 2658 }
// This is not impacted by `scale_factor`, even if that's a multiple like "2".
// i.e., positioning the app at `monitor.size().width/2` and `monitor.size().height/2`
// will place the **top left corner of the app** in roughly the center of the monitor.
let monitor_size = monitor.size();
println!("Current monitor - size: {:?}", monitor_size);
println!("Current monitor - scale factor: {:?}", monitor.scale_factor());
println!("Current monitor - position: {:?}", monitor.position());
let width_padding = monitor_size.width.div_ceil(6);
let height_padding = monitor_size.height.div_ceil(6);
let max_width = monitor_size.width.saturating_sub(width_padding);
let max_height = monitor_size.height.saturating_sub(height_padding);
let max_width = i32::try_from(max_width)?;
let max_height = i32::try_from(max_height)?;
// Prevent the window from being positioned off the screen to the bottom, right, and bottom right.
// This works on MacBook (single screen), but should be tested on Windows, Linux, and multiscreen setups.
if position.x > max_width {
return Err(anyhow::anyhow!("X position must be less than or equal to {} (monitor width {})",
max_width, monitor_size.width));
} else if position.y > max_height {
return Err(anyhow::anyhow!("Y position must be less than or equal to {} (monitor height {})",
max_height, monitor_size.height));
}
}
window.set_position(PhysicalPosition::new(position.x, position.y))?;
Ok(())
}
@@ -0,0 +1,63 @@
use crate::core::state::window::main_window_size::MainWindowSize;
use crate::core::windows::main_window::constants::{MAIN_WINDOW_MIN_HEIGHT, MAIN_WINDOW_MIN_WIDTH, MAIN_WINDOW_NAME};
use tauri::{AppHandle, Manager, PhysicalSize};
/// Try to resize the main window at startup.
/// If it doesn't meet certain sizing heuristics, abandon manual resize and let the OS
/// decide how to size the app.
pub fn resize_main_window(
app: &AppHandle,
size: &MainWindowSize,
) -> errors::AnyhowResult<()> {
let mut resize_width = size.width;
let mut resize_height = size.height;
let mut resize_was_corrected = false;
if resize_width < MAIN_WINDOW_MIN_WIDTH {
resize_width = MAIN_WINDOW_MIN_WIDTH;
resize_was_corrected = true;
}
if resize_height < MAIN_WINDOW_MIN_HEIGHT {
resize_height = MAIN_WINDOW_MIN_HEIGHT;
resize_was_corrected = true;
}
let windows = app.windows();
let window = windows.get(MAIN_WINDOW_NAME)
.ok_or_else(|| anyhow::anyhow!("Main window not found"))?;
if let Ok(Some(monitor)) = window.current_monitor() {
let scale_factor = monitor.scale_factor();
if scale_factor > 1.0 {
let min_width = MAIN_WINDOW_MIN_WIDTH as f64 * scale_factor;
let min_height = MAIN_WINDOW_MIN_HEIGHT as f64 * scale_factor;
// Is this safe? https://users.rust-lang.org/t/convert-an-f64-to-an-i32/50991/4
let min_width = min_width as u32;
let min_height = min_height as u32;
if resize_width < min_width {
resize_width = min_width;
resize_was_corrected = true;
}
if resize_height < min_height {
resize_height = min_height;
resize_was_corrected = true;
}
}
}
if resize_was_corrected {
println!("Resizing window to: {}x{} (corrected from {}x{})",
resize_width, resize_height, size.width, size.height);
} else {
println!("Resizing window to: {}x{} (no correction needed)", resize_width, resize_height);
}
window.set_size(PhysicalSize::new(resize_width, resize_height))?;
Ok(())
}
@@ -2,3 +2,11 @@
/// The name of the main application window.
/// This is the Artcraft application UX itself.
pub const MAIN_WINDOW_NAME : &str = "main";
// TODO: handle pixel density, eg. retina displays
/// This is the minimum width to resize the main window in an automated fashion
pub const MAIN_WINDOW_MIN_WIDTH : u32 = 800;
// TODO: handle pixel density, eg. retina displays
/// This is the minimum height to resize the main window in an automated fashion
pub const MAIN_WINDOW_MIN_HEIGHT : u32 = 600;
+16 -82
View File
@@ -19,12 +19,9 @@ use crate::core::lifecycle::startup::handle_tauri_startup::handle_tauri_startup;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::app_preferences::app_preferences_manager::load_app_preferences_or_default;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::main_window_position::MainWindowPosition;
use crate::core::state::main_window_size::MainWindowSize;
use crate::core::state::provider_priority::ProviderPriorityStore;
use crate::core::threads::discord_presence_thread::discord_presence_thread;
use crate::core::threads::main_window_thread::main_window_thread::main_window_thread;
use crate::core::utils::webview_unsafe::webview_unsafe_for_app;
use crate::services::fal::commands::fal_background_removal_command::fal_background_removal_command;
use crate::services::fal::commands::fal_hunyuan_image_to_3d_command::fal_hunyuan_image_to_3d_command;
use crate::services::fal::commands::fal_kling_image_to_video_command::fal_kling_image_to_video_command;
@@ -87,36 +84,12 @@ pub fn run() {
let app_env_configs_2 = app_env_configs.clone();
let provider_priority = match ProviderPriorityStore::from_filesystem_configs(&app_data_root) {
Ok(Some(priority)) => {
println!("Loaded provider priority from disk: {:?}", priority.get_priority());
priority
}
Ok(None) => {
println!("No provider priority found on disk, using default.");
ProviderPriorityStore::default()
}
Err(err) => {
eprintln!("Failed to read provider priority from disk: {:?}", err);
ProviderPriorityStore::default()
}
};
println!("Initializing backend runtime...");
tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_upload::init())
.plugin(tauri_plugin_log::Builder::new()
.level(log::LevelFilter::Info)
.targets(vec![
Target::new(TargetKind::Stdout),
Target::new(TargetKind::LogDir {
file_name: Some(app_data_root.log_file_name_str().to_string())
}),
])
.build())
.setup(move |app| {
// TODO(bt): This is broken on windows
// log_environment_details();
@@ -133,66 +106,28 @@ pub fn run() {
let root = app_data_root_2.clone();
let env_config = app_env_configs_2.clone();
let storyteller_creds = storyteller_creds_manager_2.clone();
let sora_creds = sora_creds_manager_2.clone();
let sora_tasks = sora_task_queue_2.clone();
let fal_creds = fal_creds_manager_2.clone();
let fal_tasks = fal_task_queue_2.clone();
tauri::async_runtime::block_on(async move {
if let Err(err) = handle_tauri_startup(handle, root, env_config, storyteller_creds).await {
let result = handle_tauri_startup(
handle,
root,
env_config,
storyteller_creds,
sora_creds,
sora_tasks,
fal_creds,
fal_tasks,
).await;
if let Err(err) = result {
error!("Failed to handle Tauri startup: {:?}", err);
panic!("Failed to handle Tauri startup: {:?}", err);
}
});
let result = webview_unsafe_for_app(&app);
if let Err(err) = result {
eprintln!("Error setting webview unsafe: {:?}", err);
}
// TODO(bt): Clean this up. We can just clone at the callsite. Also clean initialization
let app_2 = app.clone();
let app_3 = app.clone();
let app_4 = app.clone();
let app_5 = app.clone();
let app_env_configs_3 = app_env_configs_2.clone();
let app_data_root_3 = app_data_root_2.clone();
let app_data_root_4 = app_data_root_2.clone();
let app_data_root_5 = app_data_root_2.clone();
let sora_creds_manager_3 = sora_creds_manager_2.clone();
let storyteller_creds_manager_3 = storyteller_creds_manager.clone();
let storyteller_creds_manager_4 = storyteller_creds_manager.clone();
let fal_creds_manager_3 = fal_creds_manager_2.clone();
let fal_task_queue_3 = fal_task_queue_2.clone();
match MainWindowSize::from_filesystem_configs(&app_data_root_3) {
Ok(None) => {}
Ok(Some(size)) => {
println!("Resizing window to: {:?}", size);
let result = size.apply_to_main_window(&app);
if let Err(err) = result {
eprintln!("Could not set window size: {:?}", err);
}
}
Err(err) => {
eprintln!("Failed to read window size from disk: {:?}", err);
}
}
match MainWindowPosition::from_filesystem_configs(&app_data_root_3) {
Ok(None) => {}
Ok(Some(pos)) => {
println!("Moving window to: {:?}", pos);
let result = pos.apply_to_main_window(&app);
if let Err(err) = result {
eprintln!("Could not set window position: {:?}", err);
}
}
Err(err) => {
eprintln!("Failed to read window position from disk: {:?}", err);
}
}
tauri::async_runtime::spawn(main_window_thread(app_3, app_data_root_3, storyteller_creds_manager_2));
tauri::async_runtime::spawn(sora_task_polling_thread(app_4, app_env_configs_2, app_data_root_4, sora_creds_manager_3, storyteller_creds_manager_3, sora_task_queue_2));
tauri::async_runtime::spawn(discord_presence_thread());
tauri::async_runtime::spawn(fal_task_polling_thread(app_5, app_env_configs_3, app_data_root_5, fal_creds_manager_3, fal_task_queue_3, storyteller_creds_manager_4));
Ok(())
})
@@ -201,7 +136,6 @@ pub fn run() {
.manage(app_preferences)
.manage(fal_creds_manager)
.manage(fal_task_queue)
.manage(provider_priority)
.manage(sora_creds_manager)
.manage(sora_task_queue)
.manage(storyteller_creds_manager_3)
@@ -1,6 +1,7 @@
use crate::connection::TaskDbConnection;
use crate::error::SqliteTasksError;
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_model_type::TaskModelType;
use enums::tauri::tasks::task_status::TaskStatus;
use enums::tauri::tasks::task_type::TaskType;
use tokens::tokens::sqlite::tasks::TaskId;
@@ -9,6 +10,7 @@ pub struct CreateTaskArgs<'a> {
pub db: &'a TaskDbConnection,
pub status: TaskStatus,
pub task_type: TaskType,
pub model_type: Option<TaskModelType>,
pub provider: GenerationProvider,
pub provider_job_id: Option<&'a str>,
pub frontend_subscriber_id: Option<&'a str>,
@@ -24,6 +26,7 @@ pub async fn create_task(
let task_id_temp = task_id.as_str();
let status_temp = args.status.to_str();
let task_type_temp = args.task_type.to_str();
let model_type_temp = args.model_type.map(|s| s.to_str());
let provider_temp = args.provider.to_string();
let query = sqlx::query!(r#"
@@ -31,16 +34,18 @@ pub async fn create_task(
id,
task_status,
task_type,
model_type,
provider,
provider_job_id,
frontend_subscriber_id,
frontend_subscriber_payload
)
VALUES (?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
"#,
task_id_temp,
status_temp,
task_type_temp,
model_type_temp,
provider_temp,
args.provider_job_id,
args.frontend_subscriber_id,
@@ -1,6 +1,7 @@
use crate::connection::TaskDbConnection;
use crate::error::SqliteTasksError;
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_model_type::TaskModelType;
use enums::tauri::tasks::task_status::TaskStatus;
use enums::tauri::tasks::task_type::TaskType;
use sqlx::{QueryBuilder, Sqlite};
@@ -21,6 +22,7 @@ pub struct Task {
pub id: TaskId,
pub status: TaskStatus,
pub task_type: TaskType,
pub model_type: TaskModelType,
pub provider: GenerationProvider,
pub provider_job_id: Option<String>,
pub frontend_subscriber_id: Option<String>,
@@ -33,6 +35,7 @@ struct RawTask {
id: String,
task_status: String,
task_type: String,
model_type: String,
provider: String,
provider_job_id: Option<String>,
frontend_subscriber_id: Option<String>,
@@ -48,6 +51,7 @@ pub async fn list_tasks_by_provider_and_tokens(
id,
task_status,
task_type,
model_type,
provider,
provider_job_id,
frontend_subscriber_id,
@@ -87,6 +91,8 @@ pub async fn list_tasks_by_provider_and_tokens(
.map_err(|err| SqliteTasksError::TaskParseError(err))?,
task_type: TaskType::from_str(&task.task_type)
.map_err(|err| SqliteTasksError::TaskParseError(err))?,
model_type: TaskModelType::from_str(&task.model_type)
.map_err(|err| SqliteTasksError::TaskParseError(err))?,
provider: GenerationProvider::from_str(&task.provider)
.map_err(|err| SqliteTasksError::TaskParseError(err))?,
provider_job_id: task.provider_job_id,
@@ -1,2 +1,3 @@
pub mod task_model_type;
pub mod task_status;
pub mod task_type;
@@ -0,0 +1,241 @@
use std::collections::BTreeSet;
#[cfg(test)]
use strum::EnumCount;
#[cfg(test)]
use strum::EnumIter;
use utoipa::ToSchema;
#[cfg_attr(test, derive(EnumIter, EnumCount))]
#[derive(Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum TaskModelType {
// Image models
#[serde(rename = "flux_1_dev")]
Flux1Dev,
#[serde(rename = "flux_1_schnell")]
Flux1Schnell,
#[serde(rename = "flux_pro_1.1")]
FluxPro11,
#[serde(rename = "flux_pro_1.1_ultra")]
FluxPro11Ultra,
#[serde(rename = "gpt_image_1")]
GptImage1,
#[serde(rename = "recraft_3")]
Recraft3,
// Video models
#[serde(rename = "kling_1.6_pro")]
Kling16Pro,
#[serde(rename = "kling_2.1_pro")]
Kling21Pro,
#[serde(rename = "kling_2.1_master")]
Kling21Master,
#[serde(rename = "seedance_1.0_lite")]
Seedance10Lite,
#[serde(rename = "veo_2")]
Veo2,
// 3D Object generation models
#[serde(rename = "hunyuan_3d_2.0")]
Hunyuan3d2_0,
#[serde(rename = "hunyuan_3d_2.1")]
Hunyuan3d2_1,
}
impl_enum_display_and_debug_using_to_str!(TaskModelType);
//impl_mysql_enum_coders!(TaskModelType);
//impl_mysql_from_row!(TaskModelType);
// NB: We can derive `sqlx::Type` instead of using `impl_mysql_enum_coders`
impl TaskModelType {
pub fn to_str(&self) -> &'static str {
match self {
// Image models
Self::Flux1Dev => "flux_1_dev",
Self::Flux1Schnell => "flux_1_schnell",
Self::FluxPro11 => "flux_pro_1.1",
Self::FluxPro11Ultra => "flux_pro_1.1_ultra",
Self::GptImage1 => "gpt_image_1",
Self::Recraft3 => "recraft_3",
// Video models
Self::Kling16Pro => "kling_1.6_pro",
Self::Kling21Pro => "kling_2.1_pro",
Self::Kling21Master => "kling_2.1_master",
Self::Seedance10Lite => "seedance_1.0_lite",
Self::Veo2 => "veo_2",
// 3D Object generation models
Self::Hunyuan3d2_0 => "hunyuan_3d_2.0",
Self::Hunyuan3d2_1 => "hunyuan_3d_2.1",
}
}
pub fn from_str(job_status: &str) -> Result<Self, String> {
match job_status {
// Image models
"flux_1_dev" => Ok(Self::Flux1Dev),
"flux_1_schnell" => Ok(Self::Flux1Schnell),
"flux_pro_1.1" => Ok(Self::FluxPro11),
"flux_pro_1.1_ultra" => Ok(Self::FluxPro11Ultra),
"gpt_image_1" => Ok(Self::GptImage1),
"recraft_3" => Ok(Self::Recraft3),
// Video models
"kling_1.6_pro" => Ok(Self::Kling16Pro),
"kling_2.1_pro" => Ok(Self::Kling21Pro),
"kling_2.1_master" => Ok(Self::Kling21Master),
"seedance_1.0_lite" => Ok(Self::Seedance10Lite),
"veo_2" => Ok(Self::Veo2),
// 3D Object generation models
"hunyuan_3d_2.0" => Ok(Self::Hunyuan3d2_0),
"hunyuan_3d_2.1" => Ok(Self::Hunyuan3d2_1),
_ => Err(format!("invalid task_state: {:?}", job_status)),
}
}
pub fn all_variants() -> BTreeSet<Self> {
// NB: BTreeSet is sorted
// NB: BTreeSet::from() isn't const, but not worth using LazyStatic, etc.
BTreeSet::from([
// Image models
Self::Flux1Dev,
Self::Flux1Schnell,
Self::FluxPro11,
Self::FluxPro11Ultra,
Self::GptImage1,
Self::Recraft3,
// Video models
Self::Kling16Pro,
Self::Kling21Pro,
Self::Kling21Master,
Self::Seedance10Lite,
Self::Veo2,
// 3D Object generation models
Self::Hunyuan3d2_0,
Self::Hunyuan3d2_1,
])
}
}
#[cfg(test)]
mod tests {
use crate::test_helpers::assert_serialization;
use crate::tauri::tasks::task_model_type::TaskModelType;
mod explicit_checks {
use super::*;
#[test]
fn test_serialization() {
// Image models
assert_serialization(TaskModelType::Flux1Dev, "flux_1_dev");
assert_serialization(TaskModelType::Flux1Schnell, "flux_1_schnell");
assert_serialization(TaskModelType::FluxPro11, "flux_pro_1.1");
assert_serialization(TaskModelType::FluxPro11Ultra, "flux_pro_1.1_ultra");
assert_serialization(TaskModelType::GptImage1, "gpt_image_1");
assert_serialization(TaskModelType::Recraft3, "recraft_3");
// Video models
assert_serialization(TaskModelType::Kling16Pro, "kling_1.6_pro");
assert_serialization(TaskModelType::Kling21Pro, "kling_2.1_pro");
assert_serialization(TaskModelType::Kling21Master, "kling_2.1_master");
assert_serialization(TaskModelType::Seedance10Lite, "seedance_1.0_lite");
assert_serialization(TaskModelType::Veo2, "veo_2");
// 3D Object generation models
assert_serialization(TaskModelType::Hunyuan3d2_0, "hunyuan_3d_2.0");
assert_serialization(TaskModelType::Hunyuan3d2_1, "hunyuan_3d_2.1");
}
#[test]
fn to_str() {
// Image models
assert_eq!(TaskModelType::Flux1Dev.to_str(), "flux_1_dev");
assert_eq!(TaskModelType::Flux1Schnell.to_str(), "flux_1_schnell");
assert_eq!(TaskModelType::FluxPro11.to_str(), "flux_pro_1.1");
assert_eq!(TaskModelType::FluxPro11Ultra.to_str(), "flux_pro_1.1_ultra");
assert_eq!(TaskModelType::GptImage1.to_str(), "gpt_image_1");
assert_eq!(TaskModelType::Recraft3.to_str(), "recraft_3");
// Video models
assert_eq!(TaskModelType::Kling16Pro.to_str(), "kling_1.6_pro");
assert_eq!(TaskModelType::Kling21Pro.to_str(), "kling_2.1_pro");
assert_eq!(TaskModelType::Kling21Master.to_str(), "kling_2.1_master");
assert_eq!(TaskModelType::Seedance10Lite.to_str(), "seedance_1.0_lite");
assert_eq!(TaskModelType::Veo2.to_str(), "veo_2");
// 3D Object generation models
assert_eq!(TaskModelType::Hunyuan3d2_0.to_str(), "hunyuan_3d_2.0");
assert_eq!(TaskModelType::Hunyuan3d2_1.to_str(), "hunyuan_3d_2.1");
}
#[test]
fn from_str() {
// Image models
assert_eq!(TaskModelType::from_str("flux_1_dev").unwrap(), TaskModelType::Flux1Dev);
assert_eq!(TaskModelType::from_str("flux_1_schnell").unwrap(), TaskModelType::Flux1Schnell);
assert_eq!(TaskModelType::from_str("flux_pro_1.1").unwrap(), TaskModelType::FluxPro11);
assert_eq!(TaskModelType::from_str("flux_pro_1.1_ultra").unwrap(), TaskModelType::FluxPro11Ultra);
assert_eq!(TaskModelType::from_str("gpt_image_1").unwrap(), TaskModelType::GptImage1);
assert_eq!(TaskModelType::from_str("recraft_3").unwrap(), TaskModelType::Recraft3);
// Video models
assert_eq!(TaskModelType::from_str("kling_1.6_pro").unwrap(), TaskModelType::Kling16Pro);
assert_eq!(TaskModelType::from_str("kling_2.1_pro").unwrap(), TaskModelType::Kling21Pro);
assert_eq!(TaskModelType::from_str("kling_2.1_master").unwrap(), TaskModelType::Kling21Master);
assert_eq!(TaskModelType::from_str("seedance_1.0_lite").unwrap(), TaskModelType::Seedance10Lite);
assert_eq!(TaskModelType::from_str("veo_2").unwrap(), TaskModelType::Veo2);
// 3D Object generation models
assert_eq!(TaskModelType::from_str("hunyuan_3d_2.0").unwrap(), TaskModelType::Hunyuan3d2_0);
assert_eq!(TaskModelType::from_str("hunyuan_3d_2.1").unwrap(), TaskModelType::Hunyuan3d2_1);
}
#[test]
fn all_variants() {
let mut variants = TaskModelType::all_variants();
assert_eq!(variants.len(), 13);
// Image models
assert_eq!(variants.pop_first(), Some(TaskModelType::Flux1Dev));
assert_eq!(variants.pop_first(), Some(TaskModelType::Flux1Schnell));
assert_eq!(variants.pop_first(), Some(TaskModelType::FluxPro11));
assert_eq!(variants.pop_first(), Some(TaskModelType::FluxPro11Ultra));
assert_eq!(variants.pop_first(), Some(TaskModelType::GptImage1));
assert_eq!(variants.pop_first(), Some(TaskModelType::Recraft3));
// Video models
assert_eq!(variants.pop_first(), Some(TaskModelType::Kling16Pro));
assert_eq!(variants.pop_first(), Some(TaskModelType::Kling21Pro));
assert_eq!(variants.pop_first(), Some(TaskModelType::Kling21Master));
assert_eq!(variants.pop_first(), Some(TaskModelType::Seedance10Lite));
assert_eq!(variants.pop_first(), Some(TaskModelType::Veo2));
// 3D Object generation models
assert_eq!(variants.pop_first(), Some(TaskModelType::Hunyuan3d2_0));
assert_eq!(variants.pop_first(), Some(TaskModelType::Hunyuan3d2_1));
assert_eq!(variants.pop_first(), None);
}
}
mod mechanical_checks {
use super::*;
#[test]
fn variant_length() {
use strum::IntoEnumIterator;
assert_eq!(TaskModelType::all_variants().len(), TaskModelType::iter().len());
}
#[test]
fn round_trip() {
for variant in TaskModelType::all_variants() {
// Test to_str(), from_str(), Display, and Debug.
assert_eq!(variant, TaskModelType::from_str(variant.to_str()).unwrap());
assert_eq!(variant, TaskModelType::from_str(&format!("{}", variant)).unwrap());
assert_eq!(variant, TaskModelType::from_str(&format!("{:?}", variant)).unwrap());
}
}
#[test]
fn serialized_length_ok_for_database() {
const MAX_LENGTH : usize = 24;
for variant in TaskModelType::all_variants() {
let serialized = variant.to_str();
assert!(serialized.len() > 0, "variant {:?} is too short", variant);
assert!(serialized.len() <= MAX_LENGTH, "variant {:?} is too long", variant);
}
}
}
}