mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-21 01:12:40 -04:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16f93cba52 | ||
|
|
045aefcd13 | ||
|
|
5f6bee5a87 | ||
|
|
068ff6ac10 | ||
|
|
2b5bedf131 |
+32
-8
@@ -2,15 +2,20 @@ use crate::core::commands::enqueue::image::handle_image_artcraft::handle_image_a
|
||||
use crate::core::commands::enqueue::image::handle_image_fal::handle_image_fal;
|
||||
use crate::core::commands::enqueue::image::handle_image_sora::handle_image_sora;
|
||||
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
|
||||
use crate::core::commands::enqueue::image::success_event::SuccessEvent;
|
||||
use crate::core::commands::enqueue::object::handle_object_artcraft::handle_object_artcraft;
|
||||
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
|
||||
use crate::core::commands::response::shorthand::Response;
|
||||
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::generation_events::common::{GenerationAction, GenerationServiceProvider};
|
||||
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
|
||||
use crate::core::events::sendable_event_trait::SendableEvent;
|
||||
use crate::core::model::image_models::ImageModel;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
|
||||
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
|
||||
use crate::core::utils::download_media_file_to_temp_dir::download_media_file_to_temp_dir;
|
||||
use crate::core::utils::get_url_file_extension::get_url_file_extension;
|
||||
use crate::core::utils::save_base64_image_to_temp_dir::save_base64_image_to_temp_dir;
|
||||
@@ -59,10 +64,6 @@ use storyteller_client::media_files::upload_image_media_file_from_file::upload_i
|
||||
use storyteller_client::utils::api_host::ApiHost;
|
||||
use tauri::{AppHandle, Emitter, Manager, State};
|
||||
use tempfile::NamedTempFile;
|
||||
use crate::core::commands::enqueue::image::success_event::SuccessEvent;
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::generation_events::common::{GenerationAction, GenerationServiceProvider};
|
||||
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct EnqueueTextToImageRequest {
|
||||
@@ -85,6 +86,8 @@ impl SerializeMarker for EnqueueTextToImageSuccessResponse {}
|
||||
pub enum EnqueueTextToImageErrorType {
|
||||
/// Caller didn't specify a model
|
||||
ModelNotSpecified,
|
||||
/// No model available for image generation
|
||||
NoProviderAvailable,
|
||||
/// Generic server error
|
||||
ServerError,
|
||||
/// No Fal API key available
|
||||
@@ -102,6 +105,7 @@ pub async fn enqueue_text_to_image_command(
|
||||
request: EnqueueTextToImageRequest,
|
||||
app_data_root: State<'_, AppDataRoot>,
|
||||
app_env_configs: State<'_, AppEnvConfigs>,
|
||||
provider_priority_store: State<'_, ProviderPriorityStore>,
|
||||
fal_creds_manager: State<'_, FalCredentialManager>,
|
||||
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
|
||||
fal_task_queue: State<'_, FalTaskQueue>,
|
||||
@@ -115,6 +119,7 @@ pub async fn enqueue_text_to_image_command(
|
||||
&app,
|
||||
request,
|
||||
&app_data_root,
|
||||
&provider_priority_store,
|
||||
&fal_creds_manager,
|
||||
&storyteller_creds_manager,
|
||||
&app_env_configs,
|
||||
@@ -137,6 +142,11 @@ pub async fn enqueue_text_to_image_command(
|
||||
error_type = EnqueueTextToImageErrorType::ModelNotSpecified;
|
||||
error_message = "No model specified for image generation";
|
||||
}
|
||||
InternalImageError::NoProviderAvailable => {
|
||||
status = CommandErrorStatus::ServerError;
|
||||
error_type = EnqueueTextToImageErrorType::NoProviderAvailable;
|
||||
error_message = "No configured provider available for image generation";
|
||||
}
|
||||
InternalImageError::NeedsFalApiKey => {
|
||||
status = CommandErrorStatus::Unauthorized;
|
||||
error_type = EnqueueTextToImageErrorType::NeedsFalApiKey;
|
||||
@@ -173,6 +183,7 @@ pub async fn handle_request(
|
||||
app: &AppHandle,
|
||||
request: EnqueueTextToImageRequest,
|
||||
app_data_root: &AppDataRoot,
|
||||
provider_priority_store: &ProviderPriorityStore,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
@@ -197,9 +208,22 @@ pub async fn handle_request(
|
||||
}
|
||||
};
|
||||
|
||||
if fal_creds_manager.has_apparent_api_token()? {
|
||||
handle_image_fal(&app, request, fal_creds_manager, fal_task_queue).await
|
||||
} else {
|
||||
handle_image_artcraft(request, &app, app_env_configs, app_data_root, storyteller_creds_manager).await
|
||||
let priority = provider_priority_store.get_priority()?;
|
||||
|
||||
for provider in priority.iter() {
|
||||
match provider {
|
||||
Provider::Sora => {} // Fallthrough
|
||||
Provider::Artcraft => {
|
||||
return handle_image_artcraft(
|
||||
request, &app, app_env_configs, app_data_root, storyteller_creds_manager).await;
|
||||
}
|
||||
Provider::Fal => {
|
||||
if fal_creds_manager.has_apparent_api_token()? {
|
||||
return handle_image_fal(&app, request, fal_creds_manager, fal_task_queue).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(InternalImageError::NoProviderAvailable)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ use storyteller_client::error::storyteller_error::StorytellerError;
|
||||
#[derive(Debug)]
|
||||
pub enum InternalImageError {
|
||||
NoModelSpecified,
|
||||
NoProviderAvailable,
|
||||
NeedsFalApiKey,
|
||||
NeedsSoraCredentials,
|
||||
NeedsStorytellerCredentials,
|
||||
|
||||
+31
-6
@@ -56,6 +56,9 @@ use storyteller_client::utils::api_host::ApiHost;
|
||||
use tauri::{AppHandle, Emitter, Manager, State};
|
||||
use tempfile::NamedTempFile;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::EnqueueTextToImageErrorType;
|
||||
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
|
||||
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct EnqueueImageTo3dObjectRequest {
|
||||
@@ -90,6 +93,8 @@ impl SerializeMarker for EnqueueImageTo3dObjectSuccessResponse {}
|
||||
pub enum EnqueueImageTo3dObjectErrorType {
|
||||
/// Caller didn't specify a model
|
||||
ModelNotSpecified,
|
||||
/// No model available for object generation
|
||||
NoProviderAvailable,
|
||||
/// Generic server error
|
||||
ServerError,
|
||||
/// No Fal API key available
|
||||
@@ -105,6 +110,7 @@ pub async fn enqueue_image_to_3d_object_command(
|
||||
app: AppHandle,
|
||||
request: EnqueueImageTo3dObjectRequest,
|
||||
app_data_root: State<'_, AppDataRoot>,
|
||||
provider_priority_store: State<'_, ProviderPriorityStore>,
|
||||
fal_creds_manager: State<'_, FalCredentialManager>,
|
||||
fal_task_queue: State<'_, FalTaskQueue>,
|
||||
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
|
||||
@@ -117,6 +123,7 @@ pub async fn enqueue_image_to_3d_object_command(
|
||||
&app,
|
||||
request,
|
||||
&app_data_root,
|
||||
&provider_priority_store,
|
||||
&fal_creds_manager,
|
||||
&storyteller_creds_manager,
|
||||
&fal_task_queue,
|
||||
@@ -134,7 +141,12 @@ pub async fn enqueue_image_to_3d_object_command(
|
||||
InternalObjectError::NoModelSpecified => {
|
||||
status = CommandErrorStatus::BadRequest;
|
||||
error_type = EnqueueImageTo3dObjectErrorType::ModelNotSpecified;
|
||||
error_message = "No model specified for image generation";
|
||||
error_message = "No model specified for object generation";
|
||||
}
|
||||
InternalObjectError::NoProviderAvailable => {
|
||||
status = CommandErrorStatus::ServerError;
|
||||
error_type = EnqueueImageTo3dObjectErrorType::NoProviderAvailable;
|
||||
error_message = "No configured provider available for object generation";
|
||||
}
|
||||
InternalObjectError::NeedsFalApiKey => {
|
||||
status = CommandErrorStatus::Unauthorized;
|
||||
@@ -167,16 +179,29 @@ pub async fn handle_request(
|
||||
app: &AppHandle,
|
||||
request: EnqueueImageTo3dObjectRequest,
|
||||
app_data_root: &AppDataRoot,
|
||||
provider_priority_store: &ProviderPriorityStore,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
fal_task_queue: &FalTaskQueue,
|
||||
) -> Result<(), InternalObjectError> {
|
||||
|
||||
if fal_creds_manager.has_apparent_api_token()? {
|
||||
handle_object_fal(&app, app_data_root, request, fal_creds_manager, fal_task_queue).await?;
|
||||
} else {
|
||||
handle_object_artcraft(request, &app, app_data_root, storyteller_creds_manager).await?;
|
||||
let priority = provider_priority_store.get_priority()?;
|
||||
|
||||
for provider in priority.iter() {
|
||||
match provider {
|
||||
Provider::Sora => {} // Fallthrough
|
||||
Provider::Artcraft => {
|
||||
return Ok(handle_object_artcraft(
|
||||
request, &app, app_data_root, storyteller_creds_manager).await?);
|
||||
}
|
||||
Provider::Fal => {
|
||||
if fal_creds_manager.has_apparent_api_token()? {
|
||||
return Ok(handle_object_fal(
|
||||
&app, app_data_root, request, fal_creds_manager, fal_task_queue).await?);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Err(InternalObjectError::NoProviderAvailable)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use storyteller_client::error::storyteller_error::StorytellerError;
|
||||
#[derive(Debug)]
|
||||
pub enum InternalObjectError {
|
||||
NoModelSpecified,
|
||||
NoProviderAvailable,
|
||||
NeedsFalApiKey,
|
||||
NeedsSoraCredentials,
|
||||
NeedsStorytellerCredentials,
|
||||
|
||||
+31
-7
@@ -4,7 +4,6 @@ use crate::core::commands::enqueue::video::internal_video_error::InternalVideoEr
|
||||
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
|
||||
use crate::core::commands::response::shorthand::Response;
|
||||
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
|
||||
use crate::core::events::sendable_event_trait::SendableEvent;
|
||||
use crate::core::model::video_models::VideoModel;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
|
||||
@@ -57,6 +56,9 @@ use storyteller_client::utils::api_host::ApiHost;
|
||||
use tauri::{AppHandle, Emitter, Manager, State};
|
||||
use tempfile::NamedTempFile;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use crate::core::commands::enqueue::object::enqueue_image_to_3d_object_command::EnqueueImageTo3dObjectErrorType;
|
||||
use crate::core::commands::enqueue::object::internal_object_error::InternalObjectError;
|
||||
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct EnqueueImageToVideoRequest {
|
||||
@@ -79,6 +81,8 @@ impl SerializeMarker for EnqueueImageToVideoSuccessResponse {}
|
||||
pub enum EnqueueImageToVideoErrorType {
|
||||
/// Caller didn't specify a model
|
||||
ModelNotSpecified,
|
||||
/// No model available for video generation
|
||||
NoProviderAvailable,
|
||||
/// Generic server error
|
||||
ServerError,
|
||||
/// No Fal API key available
|
||||
@@ -94,6 +98,7 @@ pub async fn enqueue_image_to_video_command(
|
||||
app: AppHandle,
|
||||
request: EnqueueImageToVideoRequest,
|
||||
app_data_root: State<'_, AppDataRoot>,
|
||||
provider_priority_store: State<'_, ProviderPriorityStore>,
|
||||
fal_creds_manager: State<'_, FalCredentialManager>,
|
||||
fal_task_queue: State<'_, FalTaskQueue>,
|
||||
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
|
||||
@@ -106,6 +111,7 @@ pub async fn enqueue_image_to_video_command(
|
||||
&app,
|
||||
request,
|
||||
&app_data_root,
|
||||
&provider_priority_store,
|
||||
&fal_creds_manager,
|
||||
&storyteller_creds_manager,
|
||||
&fal_task_queue,
|
||||
@@ -123,7 +129,12 @@ pub async fn enqueue_image_to_video_command(
|
||||
InternalVideoError::NoModelSpecified => {
|
||||
status = CommandErrorStatus::BadRequest;
|
||||
error_type = EnqueueImageToVideoErrorType::ModelNotSpecified;
|
||||
error_message = "No model specified for image generation";
|
||||
error_message = "No model specified for video generation";
|
||||
}
|
||||
InternalVideoError::NoProviderAvailable => {
|
||||
status = CommandErrorStatus::ServerError;
|
||||
error_type = EnqueueImageToVideoErrorType::NoProviderAvailable;
|
||||
error_message = "No configured provider available for video generation";
|
||||
}
|
||||
InternalVideoError::NeedsFalApiKey => {
|
||||
status = CommandErrorStatus::Unauthorized;
|
||||
@@ -156,16 +167,29 @@ pub async fn handle_request(
|
||||
app: &AppHandle,
|
||||
request: EnqueueImageToVideoRequest,
|
||||
app_data_root: &AppDataRoot,
|
||||
provider_priority_store: &ProviderPriorityStore,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
fal_task_queue: &FalTaskQueue,
|
||||
) -> Result<(), InternalVideoError> {
|
||||
|
||||
if fal_creds_manager.has_apparent_api_token()? {
|
||||
handle_video_fal(&app, app_data_root, request, fal_creds_manager, fal_task_queue).await?;
|
||||
} else {
|
||||
handle_video_artcraft(request, &app, app_data_root, storyteller_creds_manager).await?;
|
||||
let priority = provider_priority_store.get_priority()?;
|
||||
|
||||
for provider in priority.iter() {
|
||||
match provider {
|
||||
Provider::Sora => {} // Fallthrough
|
||||
Provider::Artcraft => {
|
||||
return Ok(handle_video_artcraft(
|
||||
request, &app, app_data_root, storyteller_creds_manager).await?);
|
||||
}
|
||||
Provider::Fal => {
|
||||
if fal_creds_manager.has_apparent_api_token()? {
|
||||
return Ok(handle_video_fal(
|
||||
&app, app_data_root, request, fal_creds_manager, fal_task_queue).await?);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Err(InternalVideoError::NoProviderAvailable)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use storyteller_client::error::storyteller_error::StorytellerError;
|
||||
#[derive(Debug)]
|
||||
pub enum InternalVideoError {
|
||||
NoModelSpecified,
|
||||
NoProviderAvailable,
|
||||
NeedsFalApiKey,
|
||||
NeedsSoraCredentials,
|
||||
NeedsStorytellerCredentials,
|
||||
|
||||
@@ -3,4 +3,5 @@ pub mod enqueue;
|
||||
pub mod flip_image;
|
||||
pub mod get_app_info_command;
|
||||
pub mod platform_info_command;
|
||||
pub mod providers;
|
||||
pub mod response;
|
||||
@@ -0,0 +1,33 @@
|
||||
use crate::core::commands::response::shorthand::ResponseOrErrorMessage;
|
||||
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
|
||||
use crate::core::state::app_preferences::app_preferences_manager::AppPreferencesManager;
|
||||
use crate::core::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory;
|
||||
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
|
||||
use errors::AnyhowResult;
|
||||
use log::{error, info};
|
||||
use serde_derive::Serialize;
|
||||
use tauri::{AppHandle, State};
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct GetProviderOrderResponse {
|
||||
pub providers: Vec<Provider>,
|
||||
}
|
||||
|
||||
impl SerializeMarker for GetProviderOrderResponse{}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_provider_order_command(
|
||||
provider_priority_store: State<'_, ProviderPriorityStore>,
|
||||
) -> ResponseOrErrorMessage<GetProviderOrderResponse> {
|
||||
info!("get_provider_order_command called");
|
||||
|
||||
let providers = provider_priority_store.get_priority()
|
||||
.map_err(|err| {
|
||||
error!("Failed to get provider order: {:?}", err);
|
||||
"Failed to retrieve provider order"
|
||||
})?;
|
||||
|
||||
Ok(GetProviderOrderResponse {
|
||||
providers,
|
||||
}.into())
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod get_provider_order_command;
|
||||
pub mod set_provider_order_command;
|
||||
@@ -0,0 +1,39 @@
|
||||
use crate::core::commands::response::shorthand::{ResponseOrErrorMessage, SuccessOrErrorMessage};
|
||||
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
|
||||
use crate::core::model::image_models::ImageModel;
|
||||
use crate::core::state::app_preferences::app_preferences_manager::AppPreferencesManager;
|
||||
use crate::core::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
|
||||
use errors::AnyhowResult;
|
||||
use log::{error, info};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use tauri::{AppHandle, State};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SetProviderOrderRequest {
|
||||
pub providers: Vec<Provider>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn set_provider_order_command(
|
||||
request: SetProviderOrderRequest,
|
||||
provider_priority_store: State<'_, ProviderPriorityStore>,
|
||||
app_data_root: State<'_, AppDataRoot>,
|
||||
) -> SuccessOrErrorMessage {
|
||||
info!("set_provider_order_command called");
|
||||
|
||||
provider_priority_store.set_priority(&request.providers)
|
||||
.map_err(|err| {
|
||||
error!("Failed to set provider order: {:?}", err);
|
||||
"Failed to set provider order"
|
||||
})?;
|
||||
|
||||
provider_priority_store.persist_to_filesystem(&app_data_root)
|
||||
.map_err(|err| {
|
||||
error!("Failed to persist provider order to filesystem: {:?}", err);
|
||||
"Failed to persist provider order"
|
||||
})?;
|
||||
|
||||
Ok(().into())
|
||||
}
|
||||
@@ -30,3 +30,19 @@ pub type Response<SuccessPayload, ErrType, ErrPayload> =
|
||||
|
||||
/// No inner payloads for this type. Just strings as messages.
|
||||
pub type SimpleResponse = Response<(), (), ()>;
|
||||
|
||||
/// Either an empty success payload or error message.
|
||||
pub type SuccessOrErrorMessage =
|
||||
Result<CommandSuccessResponseWrapper<()>, CommandErrorResponseWrapper<(), ()>>;
|
||||
|
||||
/// Either a success or error message.
|
||||
pub type ResponseOrErrorMessage<SuccessPayload> =
|
||||
Result<CommandSuccessResponseWrapper<SuccessPayload>, CommandErrorResponseWrapper<(), ()>>;
|
||||
|
||||
/// Either a success or error type.
|
||||
pub type ResponseOrErrorType<SuccessPayload, ErrType> =
|
||||
Result<CommandSuccessResponseWrapper<SuccessPayload>, CommandErrorResponseWrapper<ErrType, ()>>;
|
||||
|
||||
/// Either a success or error payload.
|
||||
pub type ResponseOrError<SuccessPayload, ErrPayload> =
|
||||
Result<CommandSuccessResponseWrapper<SuccessPayload>, CommandErrorResponseWrapper<(), ErrPayload>>;
|
||||
|
||||
@@ -36,6 +36,10 @@ impl AppSettingsDir {
|
||||
self.path.join("env_configs.json")
|
||||
}
|
||||
|
||||
pub fn get_provider_preferences_path(&self) -> PathBuf {
|
||||
self.path.join("provider_preferences.json")
|
||||
}
|
||||
|
||||
pub fn write_app_preferences(&self, prefs: &AppPreferences) -> AnyhowResult<()> {
|
||||
let serializable = prefs.to_serializable();
|
||||
|
||||
|
||||
@@ -5,4 +5,5 @@ pub mod data_dir;
|
||||
pub mod expanduser;
|
||||
pub mod main_window_position;
|
||||
pub mod main_window_size;
|
||||
pub mod os_platform;
|
||||
pub mod os_platform;
|
||||
pub mod provider_priority;
|
||||
@@ -0,0 +1,162 @@
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use errors::AnyhowResult;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Provider {
|
||||
Artcraft,
|
||||
Fal,
|
||||
Sora,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct ProviderPriority {
|
||||
/// Providers may occur zero or one time.
|
||||
/// They are ordered by priority, highest to lowest.
|
||||
provider_priority: Vec<Provider>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ProviderPriorityStore {
|
||||
provider_priority: Arc<RwLock<ProviderPriority>>,
|
||||
}
|
||||
|
||||
impl ProviderPriority {
|
||||
pub fn default() -> Self {
|
||||
Self {
|
||||
provider_priority: vec![
|
||||
Provider::Artcraft,
|
||||
Provider::Sora,
|
||||
Provider::Fal,
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_priority(&mut self, ordered_list: &[Provider]) -> AnyhowResult<()> {
|
||||
let set : HashSet<&Provider> = HashSet::from_iter(ordered_list.iter());
|
||||
|
||||
if set.len() != ordered_list.len() {
|
||||
return Err(anyhow::anyhow!("Duplicate providers in the ordered list"));
|
||||
}
|
||||
|
||||
self.provider_priority = ordered_list.to_vec();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_priority(&self) -> &[Provider] {
|
||||
&self.provider_priority
|
||||
}
|
||||
|
||||
pub fn from_filesystem_configs(app_data_root: &AppDataRoot) -> AnyhowResult<Option<Self>> {
|
||||
let filename = app_data_root.settings_dir().get_provider_preferences_path();
|
||||
if !filename.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
let contents = std::fs::read_to_string(filename)?;
|
||||
let providers : Self = serde_json::from_str(&contents)?;
|
||||
Ok(Some(providers))
|
||||
}
|
||||
|
||||
pub fn persist_to_filesystem(&self, app_data_root: &AppDataRoot) -> AnyhowResult<()> {
|
||||
let filename = app_data_root.settings_dir().get_provider_preferences_path();
|
||||
let json = serde_json::to_string(self)?;
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(filename)?;
|
||||
file.write_all(json.as_bytes())?;
|
||||
file.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ProviderPriorityStore {
|
||||
pub fn default() -> Self {
|
||||
Self {
|
||||
provider_priority: Arc::new(RwLock::new(ProviderPriority::default())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_priority(&self, ordered_list: &[Provider]) -> AnyhowResult<()> {
|
||||
match self.provider_priority.write() {
|
||||
Err(err) => {
|
||||
Err(anyhow::anyhow!("Failed to acquire write lock on provider priority: {}", err))
|
||||
}
|
||||
Ok(mut providers) => {
|
||||
providers.set_priority(ordered_list)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_priority(&self) -> AnyhowResult<Vec<Provider>> {
|
||||
match self.provider_priority.read() {
|
||||
Err(err) => {
|
||||
Err(anyhow::anyhow!("Failed to acquire read lock on provider priority: {}", err))
|
||||
}
|
||||
Ok(providers) => {
|
||||
Ok(providers.get_priority().to_vec())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_filesystem_configs(app_data_root: &AppDataRoot) -> AnyhowResult<Option<Self>> {
|
||||
match ProviderPriority::from_filesystem_configs(app_data_root) {
|
||||
Err(err) => {
|
||||
Err(anyhow::anyhow!("Failed to read provider preferences from filesystem: {}", err))
|
||||
}
|
||||
Ok(None) => Ok(None),
|
||||
Ok(Some(providers)) => {
|
||||
Ok(Some(Self {
|
||||
provider_priority: Arc::new(RwLock::new(providers)),
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn persist_to_filesystem(&self, app_data_root: &AppDataRoot) -> AnyhowResult<()> {
|
||||
match self.provider_priority.read() {
|
||||
Err(err) => {
|
||||
Err(anyhow::anyhow!("Failed to acquire read lock on provider priority: {}", err))
|
||||
}
|
||||
Ok(providers) => {
|
||||
providers.persist_to_filesystem(app_data_root)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_default_priority() {
|
||||
let priority = ProviderPriority::default();
|
||||
assert_eq!(priority.get_priority(), &[Provider::Artcraft, Provider::Sora, Provider::Fal]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_priority() -> AnyhowResult<()> {
|
||||
let mut priority = ProviderPriority::default();
|
||||
priority.set_priority(&[Provider::Fal, Provider::Artcraft])?;
|
||||
assert_eq!(priority.get_priority(), &[Provider::Fal, Provider::Artcraft]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_priority_with_duplicates() {
|
||||
let mut priority = ProviderPriority::default();
|
||||
let result = priority.set_priority(&[Provider::Artcraft, Provider::Artcraft]);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
@@ -11,11 +11,14 @@ use crate::core::commands::enqueue::video::enqueue_image_to_video_command::enque
|
||||
use crate::core::commands::flip_image::flip_image;
|
||||
use crate::core::commands::get_app_info_command::get_app_info_command;
|
||||
use crate::core::commands::platform_info_command::platform_info_command;
|
||||
use crate::core::commands::providers::get_provider_order_command::get_provider_order_command;
|
||||
use crate::core::commands::providers::set_provider_order_command::set_provider_order_command;
|
||||
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;
|
||||
@@ -77,6 +80,21 @@ pub fn run() {
|
||||
|
||||
let app_env_configs = AppEnvConfigs::load_from_filesystem(&app_data_root)
|
||||
.expect("AppEnvConfigs should be loaded from disk");
|
||||
|
||||
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...");
|
||||
|
||||
@@ -165,6 +183,7 @@ 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)
|
||||
@@ -180,6 +199,8 @@ pub fn run() {
|
||||
get_app_info_command,
|
||||
get_app_preferences_command,
|
||||
get_fal_api_key_command,
|
||||
get_provider_order_command,
|
||||
set_provider_order_command,
|
||||
open_sora_login_command,
|
||||
platform_info_command,
|
||||
set_fal_api_key_command,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::services::fal::state::read_fal_api_key_from_disk::read_fal_api_key_from_disk;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::services::fal::state::read_fal_api_key_from_disk::read_fal_api_key_from_disk;
|
||||
use anyhow::anyhow;
|
||||
use errors::AnyhowResult;
|
||||
use fal_client::creds::fal_api_key::FalApiKey;
|
||||
@@ -42,6 +42,9 @@ impl FalCredentialManager {
|
||||
}
|
||||
|
||||
pub fn set_key(&self, key: &FalApiKey) -> AnyhowResult<()> {
|
||||
if key.0.trim().is_empty() {
|
||||
return Err(anyhow!("FAL API key cannot be empty")); // TODO: Also handle upstream.
|
||||
}
|
||||
self.key.set_clone(key)
|
||||
}
|
||||
|
||||
|
||||
+46
-21
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
@@ -21,9 +21,10 @@ import {
|
||||
faGripVertical,
|
||||
faSpinnerThird,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { SetProviderOrder, Provider, GetProviderOrder } from "@storyteller/tauri-api";
|
||||
|
||||
interface RouterItem {
|
||||
id: string;
|
||||
interface ProviderItem {
|
||||
id: Provider;
|
||||
name: string;
|
||||
emoji: string;
|
||||
}
|
||||
@@ -81,16 +82,40 @@ const SortableItem = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const RouterPrioritySettingsPane = () => {
|
||||
const [items, setItems] = useState<RouterItem[]>([
|
||||
{ id: "artcraft-3d", name: "ArtCraft 3D", emoji: "🎨" },
|
||||
{ id: "gpt-image-1", name: "GPT Image 1 (GPT-4o)", emoji: "🤖" },
|
||||
{ id: "flux-pro-ultra", name: "Flux Pro Ultra", emoji: "⚡" },
|
||||
{ id: "recraft-3", name: "Recraft 3", emoji: "🌟" },
|
||||
{ id: "flux-kontext", name: "Flux.1 Kontext", emoji: "🔮" },
|
||||
]);
|
||||
const ProviderItemMap = {
|
||||
[Provider.ArtCraft]: { id: Provider.ArtCraft, name: "ArtCraft", emoji: "🎨" },
|
||||
[Provider.Fal]: { id: Provider.Fal, name: "Fal", emoji: "🤖" },
|
||||
[Provider.Sora]: { id: Provider.Sora, name: "Sora / ChatGPT", emoji: "⚡" },
|
||||
};
|
||||
|
||||
export const ProviderPrioritySettingsPane = () => {
|
||||
const [items, setItems] = useState<ProviderItem[]>([]);
|
||||
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const providers = await GetProviderOrder();
|
||||
|
||||
let items: ProviderItem[] = [];
|
||||
|
||||
// Add providers from backend (in order)
|
||||
for (let provider of providers.payload.providers) {
|
||||
items.push(ProviderItemMap[provider]);
|
||||
}
|
||||
|
||||
// Add providers not in backend (in order)
|
||||
for (const [key, value] of Object.entries(ProviderItemMap)) {
|
||||
if (!providers.payload.providers.includes(key as Provider)) {
|
||||
items.push(value)
|
||||
}
|
||||
}
|
||||
|
||||
setItems(items);
|
||||
};
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, {
|
||||
@@ -98,19 +123,19 @@ export const RouterPrioritySettingsPane = () => {
|
||||
})
|
||||
);
|
||||
|
||||
const updateRouterPriorityOnBackend = async (newOrder: RouterItem[]) => {
|
||||
const updateProviderPriorityOnBackend = async (newOrder: ProviderItem[]) => {
|
||||
try {
|
||||
setIsUpdating(true);
|
||||
console.log("Updating router priority on backend:", newOrder);
|
||||
console.log("Updating provider priority on backend:", newOrder);
|
||||
|
||||
// Simulate API call delay
|
||||
await new Promise((resolve) => setTimeout(resolve, 500)); //remove this - BFlat
|
||||
let ordering = newOrder.map((item) => item.id);
|
||||
console.log("Provider order:", ordering);
|
||||
|
||||
//api call here
|
||||
await SetProviderOrder({ providers: ordering });
|
||||
|
||||
console.log("Router priority updated successfully");
|
||||
console.log("Provider priority updated successfully");
|
||||
} catch (error) {
|
||||
console.error("Failed to update router priority:", error);
|
||||
console.error("Failed to update provider priority:", error);
|
||||
} finally {
|
||||
setIsUpdating(false);
|
||||
}
|
||||
@@ -127,7 +152,7 @@ export const RouterPrioritySettingsPane = () => {
|
||||
const newOrder = arrayMove(prevItems, oldIndex, newIndex);
|
||||
|
||||
// Send update to backend
|
||||
updateRouterPriorityOnBackend(newOrder);
|
||||
updateProviderPriorityOnBackend(newOrder);
|
||||
|
||||
return newOrder;
|
||||
});
|
||||
@@ -138,8 +163,8 @@ export const RouterPrioritySettingsPane = () => {
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p className="text-sm text-white/70 mb-4">
|
||||
Drag and drop to reorder model priority. Higher items will be tried
|
||||
first.
|
||||
Drag and drop to reorder model provider priority. Higher items will be tried
|
||||
first. You can use this to control favorite services and spending.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,7 @@ import { MiscSettingsPane } from "./panes/MiscSettingsPane";
|
||||
import { AudioSettingsPane } from "./panes/AudioSettingsPane";
|
||||
import { AccountSettingsPane } from "./panes/AccountSettings/AccountSettingsPane";
|
||||
import { AboutSettingsPane } from "./panes/AboutSettingsPane";
|
||||
import { RouterPrioritySettingsPane } from "./panes/RouterPrioritySettingsPane";
|
||||
import { ProviderPrioritySettingsPane } from "./panes/ProviderPrioritySettingsPane";
|
||||
|
||||
interface SettingsModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -27,7 +27,7 @@ type SettingsSection =
|
||||
| "accounts"
|
||||
| "alerts"
|
||||
| "about"
|
||||
| "router_priority";
|
||||
| "provider_priority";
|
||||
|
||||
export const SettingsModal = ({
|
||||
isOpen,
|
||||
@@ -41,13 +41,13 @@ export const SettingsModal = ({
|
||||
const sections = [
|
||||
{ id: "general" as const, label: "General", icon: faCog },
|
||||
{ id: "accounts" as const, label: "Accounts", icon: faUser },
|
||||
{ id: "alerts" as const, label: "Alerts", icon: faVolumeHigh },
|
||||
{ id: "about" as const, label: "About", icon: faCircleInfo },
|
||||
{
|
||||
id: "router_priority" as const,
|
||||
label: "Model Router Priority",
|
||||
id: "provider_priority" as const,
|
||||
label: "Provider Priority",
|
||||
icon: faRoute,
|
||||
},
|
||||
{ id: "alerts" as const, label: "Alerts", icon: faVolumeHigh },
|
||||
{ id: "about" as const, label: "About", icon: faCircleInfo },
|
||||
//{ id: "video" as const, label: "Video", icon: faVideo },
|
||||
//{ id: "image" as const, label: "Image", icon: faImage },
|
||||
];
|
||||
@@ -66,8 +66,8 @@ export const SettingsModal = ({
|
||||
);
|
||||
case "about":
|
||||
return <AboutSettingsPane />;
|
||||
case "router_priority":
|
||||
return <RouterPrioritySettingsPane />;
|
||||
case "provider_priority":
|
||||
return <ProviderPrioritySettingsPane />;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -17,3 +17,7 @@ export * from "./lib/sora/SoraImageRemix.js";
|
||||
export * from "./lib/sora/useSoraLoginListener.js";
|
||||
export * from "./lib/sora/waitForSoraLogin.js";
|
||||
export * from "./lib/system/GetAppInfo.js";
|
||||
export * from "./lib/providers/SetProviderOrder.js";
|
||||
export * from "./lib/providers/Provider.js";
|
||||
export * from "./lib/providers/GetProviderOrder.js";
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { CommandResult } from "../common/CommandStatus";
|
||||
import { Provider } from "./Provider";
|
||||
|
||||
export interface GetProviderOrderSuccess extends CommandResult {
|
||||
payload: GetProviderOrderPayload;
|
||||
}
|
||||
|
||||
export interface GetProviderOrderPayload {
|
||||
providers: Provider[];
|
||||
}
|
||||
|
||||
// Returns the Success and Error variants directly.
|
||||
// Throws on Network/Tauri errors.
|
||||
export const GetProviderOrder = async () : Promise<GetProviderOrderSuccess> => {
|
||||
try {
|
||||
return await invoke("get_provider_order_command") as GetProviderOrderSuccess;
|
||||
} catch (error) {
|
||||
// NB: Endpoint should be infalliable
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
|
||||
export enum Provider {
|
||||
ArtCraft = "artcraft",
|
||||
Fal = "fal",
|
||||
Sora = "sora",
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { CommandResult } from "../common/CommandStatus";
|
||||
import { Provider } from "./Provider";
|
||||
|
||||
export interface SetProviderOrderRequest {
|
||||
providers: Provider[];
|
||||
}
|
||||
|
||||
export const SetProviderOrder = async (request: SetProviderOrderRequest) : Promise<CommandResult> => {
|
||||
const result = await invoke("set_provider_order_command", {
|
||||
request: {
|
||||
providers: request.providers,
|
||||
}
|
||||
});
|
||||
|
||||
return (result as CommandResult);
|
||||
}
|
||||
Reference in New Issue
Block a user