mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-03 00:05:41 -05:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a13dc6acfd | ||
|
|
760639d071 | ||
|
|
4e68169faf | ||
|
|
8090cdb526 | ||
|
|
ada4c77093 | ||
|
|
7bb14e1b17 | ||
|
|
f9c8343165 | ||
|
|
53eb46d2b6 | ||
|
|
f5a3ff67b3 | ||
|
|
6b262d9fcb |
+25
-28
@@ -7,6 +7,7 @@ use crate::core::commands::response::failure_response_wrapper::{CommandErrorResp
|
||||
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::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;
|
||||
@@ -58,6 +59,10 @@ 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 {
|
||||
@@ -65,28 +70,9 @@ pub struct EnqueueTextToImageRequest {
|
||||
pub prompt: Option<String>,
|
||||
|
||||
/// The model to use.
|
||||
pub model: Option<EnqueueTextToImageModel>,
|
||||
pub model: Option<ImageModel>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Copy, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum EnqueueTextToImageModel {
|
||||
#[serde(rename = "flux_1_dev")]
|
||||
Flux1Dev,
|
||||
#[serde(rename = "flux_1_schnell")]
|
||||
Flux1Schnell,
|
||||
#[deprecated(note="use `flux_pro_11_ultra` instead")]
|
||||
#[serde(rename = "flux_pro_ultra")]
|
||||
FluxProUltra,
|
||||
#[serde(rename = "flux_pro_11")]
|
||||
FluxPro11,
|
||||
#[serde(rename = "flux_pro_11_ultra")]
|
||||
FluxPro11Ultra,
|
||||
#[serde(rename = "gpt_image_1")]
|
||||
GptImage1,
|
||||
#[serde(rename = "recraft_3")]
|
||||
Recraft3,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct EnqueueTextToImageSuccessResponse {
|
||||
@@ -166,7 +152,17 @@ pub async fn enqueue_text_to_image_command(
|
||||
error_details: None,
|
||||
})
|
||||
}
|
||||
Ok(()) => {
|
||||
Ok(event) => {
|
||||
let event = GenerationEnqueueSuccessEvent {
|
||||
action: GenerationAction::GenerateImage,
|
||||
service: event.service_provider,
|
||||
model: Some(event.tauri_event_model()),
|
||||
};
|
||||
|
||||
if let Err(err) = event.send(&app) {
|
||||
error!("Failed to emit event: {:?}", err); // Fail open.
|
||||
}
|
||||
|
||||
Ok(EnqueueTextToImageSuccessResponse {}.into())
|
||||
}
|
||||
}
|
||||
@@ -183,15 +179,18 @@ pub async fn handle_request(
|
||||
fal_task_queue: &FalTaskQueue,
|
||||
sora_creds_manager: &SoraCredentialManager,
|
||||
sora_task_queue: &SoraTaskQueue,
|
||||
) -> Result<(), InternalImageError> {
|
||||
) -> Result<SuccessEvent, InternalImageError> {
|
||||
|
||||
match request.model {
|
||||
None => {
|
||||
return Err(InternalImageError::NoModelSpecified);
|
||||
}
|
||||
Some(EnqueueTextToImageModel::GptImage1) => {
|
||||
Some(ImageModel::GptImage1) => {
|
||||
handle_image_sora(&app, request, sora_creds_manager, sora_task_queue).await?;
|
||||
return Ok(());
|
||||
return Ok(SuccessEvent {
|
||||
service_provider: GenerationServiceProvider::Sora,
|
||||
model: ImageModel::GptImage1,
|
||||
});
|
||||
}
|
||||
_ => {
|
||||
// Fall-through
|
||||
@@ -199,10 +198,8 @@ 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?;
|
||||
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?;
|
||||
handle_image_artcraft(request, &app, app_env_configs, app_data_root, storyteller_creds_manager).await
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageModel, EnqueueTextToImageRequest};
|
||||
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::EnqueueTextToImageRequest;
|
||||
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
|
||||
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, GenerationModel, GenerationServiceProvider};
|
||||
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
|
||||
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
|
||||
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::utils::download_media_file_to_temp_dir::download_media_file_to_temp_dir;
|
||||
@@ -41,7 +43,7 @@ pub async fn handle_image_artcraft(
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
app_data_root: &AppDataRoot,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
) -> Result<(), InternalImageError> {
|
||||
) -> Result<SuccessEvent, InternalImageError> {
|
||||
|
||||
let creds = match storyteller_creds_manager.get_credentials()? {
|
||||
Some(creds) => creds,
|
||||
@@ -62,21 +64,21 @@ pub async fn handle_image_artcraft(
|
||||
|
||||
let uuid_idempotency_token = generate_random_uuid();
|
||||
|
||||
let mut selected_model = None;
|
||||
let mut selected_model;
|
||||
|
||||
let job_token = match request.model {
|
||||
None => {
|
||||
return Err(InternalImageError::NoModelSpecified);
|
||||
}
|
||||
Some(EnqueueTextToImageModel::GptImage1) => {
|
||||
Some(ImageModel::GptImage1) => {
|
||||
return Err(InternalImageError::AnyhowError(anyhow!("wrong logic: artcraft is handling sora images")));
|
||||
}
|
||||
Some(EnqueueTextToImageModel::Recraft3) => {
|
||||
Some(ImageModel::Recraft3) => {
|
||||
return Err(InternalImageError::AnyhowError(anyhow!("not yet implemented in Artcraft")));
|
||||
}
|
||||
Some(EnqueueTextToImageModel::Flux1Dev) => {
|
||||
Some(ImageModel::Flux1Dev) => {
|
||||
info!("enqueue Flux 1 Dev");
|
||||
selected_model = Some(GenerationModel::Flux1Dev);
|
||||
selected_model = ImageModel::Flux1Dev;
|
||||
let request = GenerateFlux1DevTextToImageRequest {
|
||||
uuid_idempotency_token,
|
||||
prompt: request.prompt,
|
||||
@@ -99,9 +101,9 @@ pub async fn handle_image_artcraft(
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(EnqueueTextToImageModel::Flux1Schnell) => {
|
||||
Some(ImageModel::Flux1Schnell) => {
|
||||
info!("enqueue Flux 1 Schnell");
|
||||
selected_model = Some(GenerationModel::Flux1Schnell);
|
||||
selected_model = ImageModel::Flux1Schnell;
|
||||
let request = GenerateFlux1SchnellTextToImageRequest {
|
||||
uuid_idempotency_token,
|
||||
prompt: request.prompt,
|
||||
@@ -124,9 +126,9 @@ pub async fn handle_image_artcraft(
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(EnqueueTextToImageModel::FluxPro11) => {
|
||||
Some(ImageModel::FluxPro11) => {
|
||||
info!("enqueue Flux Pro 1.1");
|
||||
selected_model = Some(GenerationModel::FluxPro11);
|
||||
selected_model = ImageModel::FluxPro11;
|
||||
let request = GenerateFluxPro11TextToImageRequest {
|
||||
uuid_idempotency_token,
|
||||
prompt: request.prompt,
|
||||
@@ -149,12 +151,9 @@ pub async fn handle_image_artcraft(
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(
|
||||
EnqueueTextToImageModel::FluxProUltra |
|
||||
EnqueueTextToImageModel::FluxPro11Ultra
|
||||
) => {
|
||||
Some(ImageModel::FluxPro11Ultra) => {
|
||||
info!("enqueue Flux Pro 1.1 Ultra");
|
||||
selected_model = Some(GenerationModel::FluxPro11Ultra);
|
||||
selected_model = ImageModel::FluxPro11Ultra;
|
||||
let request = GenerateFluxPro11UltraTextToImageRequest {
|
||||
uuid_idempotency_token,
|
||||
prompt: request.prompt,
|
||||
@@ -179,5 +178,8 @@ pub async fn handle_image_artcraft(
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
Ok(SuccessEvent {
|
||||
service_provider: GenerationServiceProvider::Artcraft,
|
||||
model:selected_model,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageModel, EnqueueTextToImageRequest};
|
||||
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::EnqueueTextToImageRequest;
|
||||
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
|
||||
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_failure_event::GenerationEnqueueFailureEvent;
|
||||
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
|
||||
use crate::core::model::image_models::ImageModel;
|
||||
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;
|
||||
@@ -22,7 +24,7 @@ pub async fn handle_image_fal(
|
||||
request: EnqueueTextToImageRequest,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
fal_task_queue: &FalTaskQueue,
|
||||
) -> Result<(), InternalImageError> {
|
||||
) -> Result<SuccessEvent, InternalImageError> {
|
||||
|
||||
let api_key = match fal_creds_manager.get_key()? {
|
||||
Some(key) => key,
|
||||
@@ -47,31 +49,32 @@ pub async fn handle_image_fal(
|
||||
|
||||
let prompt = request.prompt.as_deref().unwrap_or("");
|
||||
|
||||
let model;
|
||||
|
||||
let result = match request.model {
|
||||
None => {
|
||||
return Err(InternalImageError::NoModelSpecified);
|
||||
}
|
||||
Some(EnqueueTextToImageModel::GptImage1) => {
|
||||
Some(ImageModel::GptImage1) => {
|
||||
return Err(InternalImageError::AnyhowError(anyhow!("wrong logic: fal is handling sora images")));
|
||||
}
|
||||
Some(
|
||||
EnqueueTextToImageModel::Flux1Dev |
|
||||
EnqueueTextToImageModel::Flux1Schnell |
|
||||
EnqueueTextToImageModel::FluxPro11
|
||||
ImageModel::Flux1Dev |
|
||||
ImageModel::Flux1Schnell |
|
||||
ImageModel::FluxPro11
|
||||
) => {
|
||||
return Err(InternalImageError::AnyhowError(anyhow!("not yet implemented: {:?}", request.model)));
|
||||
}
|
||||
Some(
|
||||
EnqueueTextToImageModel::FluxProUltra |
|
||||
EnqueueTextToImageModel::FluxPro11Ultra
|
||||
) => {
|
||||
Some(ImageModel::FluxPro11Ultra) => {
|
||||
model = ImageModel::FluxPro11Ultra;
|
||||
info!("enqueue Flux Pro 1.1 Ultra text-to-image with prompt: {}", prompt);
|
||||
enqueue_flux_pro_11_ultra_text_to_image(FluxPro11UltraTextToImageArgs {
|
||||
prompt,
|
||||
api_key: &api_key,
|
||||
}).await
|
||||
}
|
||||
Some(EnqueueTextToImageModel::Recraft3) => {
|
||||
Some(ImageModel::Recraft3) => {
|
||||
model = ImageModel::Recraft3;
|
||||
info!("enqueue Recraft v3 text-to-image with prompt: {}", prompt);
|
||||
enqueue_recraft3_text_to_image(Recraft3TextToImageArgs {
|
||||
prompt,
|
||||
@@ -84,15 +87,15 @@ pub async fn handle_image_fal(
|
||||
Ok(enqueued) => {
|
||||
info!("Successfully enqueued text to image");
|
||||
|
||||
let event = GenerationEnqueueSuccessEvent {
|
||||
action: GenerationAction::GenerateImage,
|
||||
service: GenerationServiceProvider::Fal,
|
||||
model: None,
|
||||
};
|
||||
//let event = GenerationEnqueueSuccessEvent {
|
||||
// action: GenerationAction::GenerateImage,
|
||||
// service: GenerationServiceProvider::Fal,
|
||||
// model: None,
|
||||
//};
|
||||
|
||||
if let Err(err) = event.send(app) {
|
||||
error!("Failed to emit event: {:?}", err); // Fail open.
|
||||
}
|
||||
//if let Err(err) = event.send(app) {
|
||||
// error!("Failed to emit event: {:?}", err); // Fail open.
|
||||
//}
|
||||
|
||||
if let Err(err) = fal_task_queue.insert(&enqueued) {
|
||||
error!("Failed to enqueue task: {:?}", err);
|
||||
@@ -117,5 +120,8 @@ pub async fn handle_image_fal(
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(SuccessEvent {
|
||||
service_provider: GenerationServiceProvider::Fal,
|
||||
model,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageModel, EnqueueTextToImageRequest};
|
||||
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageRequest};
|
||||
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::generation_events::common::{GenerationAction, GenerationServiceProvider};
|
||||
@@ -90,15 +90,5 @@ pub async fn handle_image_sora(
|
||||
|
||||
sora_task_queue.insert(&response.task_id)?;
|
||||
|
||||
let event = GenerationEnqueueSuccessEvent {
|
||||
action: GenerationAction::GenerateImage,
|
||||
service: GenerationServiceProvider::Sora,
|
||||
model: None,
|
||||
};
|
||||
|
||||
if let Err(err) = event.send(app) {
|
||||
error!("Failed to emit event: {:?}", err); // Fail open.
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2,4 +2,5 @@ pub mod enqueue_text_to_image_command;
|
||||
pub mod handle_image_artcraft;
|
||||
pub mod handle_image_fal;
|
||||
pub mod handle_image_sora;
|
||||
pub mod internal_image_error;
|
||||
pub mod internal_image_error;
|
||||
mod success_event;
|
||||
@@ -0,0 +1,20 @@
|
||||
use crate::core::events::generation_events::common::{GenerationModel, GenerationServiceProvider};
|
||||
use crate::core::model::image_models::ImageModel;
|
||||
|
||||
pub struct SuccessEvent {
|
||||
pub service_provider: GenerationServiceProvider,
|
||||
pub model: ImageModel,
|
||||
}
|
||||
|
||||
impl SuccessEvent {
|
||||
pub fn tauri_event_model(&self) -> GenerationModel {
|
||||
match self.model {
|
||||
ImageModel::Flux1Dev => GenerationModel::Flux1Dev,
|
||||
ImageModel::Flux1Schnell => GenerationModel::Flux1Schnell,
|
||||
ImageModel::FluxPro11 => GenerationModel::FluxPro11,
|
||||
ImageModel::FluxPro11Ultra => GenerationModel::FluxPro11Ultra,
|
||||
ImageModel::GptImage1 => GenerationModel::GptImage1,
|
||||
ImageModel::Recraft3 => GenerationModel::Recraft3,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageModel, EnqueueTextToImageRequest};
|
||||
use crate::core::commands::enqueue::object::enqueue_image_to_3d_object_command::{EnqueueImageTo3dObjectModel, EnqueueImageTo3dObjectRequest};
|
||||
use crate::core::commands::enqueue::object::internal_object_error::InternalObjectError;
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
@@ -15,6 +14,7 @@ use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use anyhow::anyhow;
|
||||
use artcraft_api_defs::generate::object::generate_hunyuan_2_0_image_to_3d::GenerateHunyuan20ImageTo3dRequest;
|
||||
use artcraft_api_defs::generate::object::generate_hunyuan_2_1_image_to_3d::GenerateHunyuan21ImageTo3dRequest;
|
||||
use fal_client::creds::fal_api_key::FalApiKey;
|
||||
use fal_client::requests::queue::enqueue_hunyuan2_image_to_3d::{enqueue_hunyuan2_image_to_3d, Hunyuan2Args};
|
||||
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
|
||||
@@ -22,10 +22,9 @@ use fal_client::requests::queue::image_gen::enqueue_recraft3_text_to_image::{enq
|
||||
use idempotency::uuid::generate_random_uuid;
|
||||
use log::{error, info, warn};
|
||||
use storyteller_client::generate::object::generate_hunyuan_3d_2_0_image_to_3d::generate_hunyuan3d_2_0_image_to_3d;
|
||||
use storyteller_client::generate::object::generate_hunyuan_3d_2_1_image_to_3d::generate_hunyuan3d_2_1_image_to_3d;
|
||||
use storyteller_client::utils::api_host::ApiHost;
|
||||
use tauri::{AppHandle, State};
|
||||
use artcraft_api_defs::generate::object::generate_hunyuan_2_1_image_to_3d::GenerateHunyuan21ImageTo3dRequest;
|
||||
use storyteller_client::generate::object::generate_hunyuan_3d_2_1_image_to_3d::generate_hunyuan3d_2_1_image_to_3d;
|
||||
|
||||
pub async fn handle_object_artcraft(
|
||||
request: EnqueueImageTo3dObjectRequest,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageModel, EnqueueTextToImageRequest};
|
||||
use crate::core::commands::enqueue::object::enqueue_image_to_3d_object_command::{EnqueueImageTo3dObjectModel, EnqueueImageTo3dObjectRequest};
|
||||
use crate::core::commands::enqueue::object::internal_object_error::InternalObjectError;
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
|
||||
-2
@@ -1,5 +1,3 @@
|
||||
use crate::core::commands::enqueue::object::handle_object_artcraft::handle_object_artcraft;
|
||||
use crate::core::commands::enqueue::object::handle_object_fal::handle_object_fal;
|
||||
use crate::core::commands::enqueue::video::handle_video_artcraft::handle_video_artcraft;
|
||||
use crate::core::commands::enqueue::video::handle_video_fal::handle_video_fal;
|
||||
use crate::core::commands::enqueue::video::internal_video_error::InternalVideoError;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageModel, EnqueueTextToImageRequest};
|
||||
use crate::core::commands::enqueue::object::enqueue_image_to_3d_object_command::{EnqueueImageTo3dObjectModel, EnqueueImageTo3dObjectRequest};
|
||||
use crate::core::commands::enqueue::video::enqueue_image_to_video_command::EnqueueImageToVideoRequest;
|
||||
use crate::core::commands::enqueue::video::internal_video_error::InternalVideoError;
|
||||
@@ -132,7 +131,6 @@ pub async fn handle_video_artcraft(
|
||||
}
|
||||
}
|
||||
Some(VideoModel::Kling21Pro) => {
|
||||
|
||||
info!("enqueue Kling 2.1 Pro with Artcraft API");
|
||||
selected_model = Some(GenerationModel::Kling21Pro);
|
||||
let request = GenerateKling21ProImageToVideoRequest {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageModel, EnqueueTextToImageRequest};
|
||||
use crate::core::commands::enqueue::video::enqueue_image_to_video_command::EnqueueImageToVideoRequest;
|
||||
use crate::core::commands::enqueue::video::internal_video_error::InternalVideoError;
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
|
||||
@@ -19,6 +19,8 @@ pub enum GenerationModel {
|
||||
FluxPro11,
|
||||
#[serde(rename = "flux_pro_1.1_ultra")]
|
||||
FluxPro11Ultra,
|
||||
#[serde(rename = "gpt_image_1")]
|
||||
GptImage1,
|
||||
#[serde(rename = "kling_1.6")]
|
||||
Kling1_6,
|
||||
#[serde(rename = "kling_2.0")]
|
||||
@@ -27,6 +29,9 @@ pub enum GenerationModel {
|
||||
Kling21Master,
|
||||
#[serde(rename = "kling_2.1_pro")]
|
||||
Kling21Pro,
|
||||
#[serde(rename = "recraft_3")]
|
||||
Recraft3,
|
||||
#[serde(rename = "sora")]
|
||||
Sora,
|
||||
#[serde(rename = "seedance_1.0_lite")]
|
||||
Seedance10Lite,
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
use serde_derive::Deserialize;
|
||||
|
||||
/// This is used in the Tauri command bridge.
|
||||
/// Don't change the serializations without coordinating with the frontend.
|
||||
#[derive(Deserialize, Debug, Copy, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ImageModel {
|
||||
#[serde(rename = "flux_1_dev")]
|
||||
Flux1Dev,
|
||||
#[serde(rename = "flux_1_schnell")]
|
||||
Flux1Schnell,
|
||||
#[serde(rename = "flux_pro_11")]
|
||||
FluxPro11,
|
||||
#[serde(rename = "flux_pro_11_ultra")]
|
||||
FluxPro11Ultra,
|
||||
#[serde(rename = "gpt_image_1")]
|
||||
GptImage1,
|
||||
#[serde(rename = "recraft_3")]
|
||||
Recraft3,
|
||||
}
|
||||
|
||||
@@ -2,12 +2,14 @@ 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::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
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;
|
||||
use crate::core::utils::clear_all_webview_cookies::clear_all_webview_cookies;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use anyhow::anyhow;
|
||||
use errors::AnyhowResult;
|
||||
use log::{error, info};
|
||||
use log::{error, info, warn};
|
||||
use memory_store::clone_slot::CloneSlot;
|
||||
use tauri::{AppHandle, Manager, Webview, Window};
|
||||
|
||||
@@ -31,6 +33,8 @@ pub async fn main_window_thread(
|
||||
.unwrap_or_else(|| "unknown".to_string()));
|
||||
info!("build timestamp: {:?}", build_metadata::build_timestamp()
|
||||
.to_string());
|
||||
|
||||
// debug_try_clear_all_webview_data(&app);
|
||||
|
||||
loop {
|
||||
for (window_name, window) in app.windows() {
|
||||
@@ -75,3 +79,15 @@ pub fn log_errors<T>(result: AnyhowResult<T>) {
|
||||
error!("Error persisting window size: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
fn debug_try_clear_all_webview_data(app: &AppHandle) {
|
||||
warn!("[!!!] THIS IS ONLY FOR DEBUGGING PURPOSES: Attempting to clear webview data...");
|
||||
|
||||
for (name, webview) in app.webviews() {
|
||||
if let Err(err) = webview.clear_all_browsing_data() {
|
||||
error!("Failed to clear cookies for '{}' window webview: {:?}", name, err);
|
||||
} else {
|
||||
warn!("Successfully cleared cookies for '{}' window webview", name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,8 +53,10 @@ pub fn run() {
|
||||
println!("Loading app preferences...");
|
||||
let app_preferences = load_app_preferences_or_default(&app_data_root);
|
||||
|
||||
println!("Attempting to read existing artcraft credentials...");
|
||||
let storyteller_creds_manager = StorytellerCredentialManager::initialize_from_disk_infallible(&app_data_root);
|
||||
// NB: tauri-plugin-http stores the credentials on disk, so we can defer to that for now.
|
||||
// println!("Attempting to read existing artcraft credentials...");
|
||||
// let storyteller_creds_manager = StorytellerCredentialManager::initialize_from_disk_infallible(&app_data_root);
|
||||
let storyteller_creds_manager = StorytellerCredentialManager::initialize_empty(&app_data_root);
|
||||
let storyteller_creds_manager_2 = storyteller_creds_manager.clone();
|
||||
let storyteller_creds_manager_3 = storyteller_creds_manager.clone();
|
||||
|
||||
|
||||
+7
@@ -20,6 +20,13 @@ pub struct StorytellerCredentialManager {
|
||||
|
||||
impl StorytellerCredentialManager {
|
||||
|
||||
pub fn initialize_empty(app_data_root: &AppDataRoot) -> Self {
|
||||
Self {
|
||||
holder: StorytellerCredentialHolder::new(),
|
||||
app_data_root: app_data_root.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn initialize_from_disk_infallible(app_data_root: &AppDataRoot) -> Self {
|
||||
let holder = StorytellerCredentialHolder::new();
|
||||
|
||||
|
||||
@@ -21,7 +21,10 @@ pub enum ApiError {
|
||||
TooManyRequests(String),
|
||||
|
||||
/// 500. An internal server error occurred.
|
||||
InternalServerError(String),
|
||||
InternalServerError {
|
||||
body: String,
|
||||
backend_hostname: Option<String>,
|
||||
},
|
||||
|
||||
/// A deserialization error with the response.
|
||||
DeserializationError(serde_json::Error),
|
||||
@@ -53,7 +56,8 @@ impl Display for ApiError {
|
||||
ApiError::Forbidden(msg) => write!(f, "Forbidden: {}", msg),
|
||||
ApiError::NotFound(msg) => write!(f, "Not found: {}", msg),
|
||||
ApiError::TooManyRequests(msg) => write!(f, "Too many requests: {}", msg),
|
||||
ApiError::InternalServerError(msg) => write!(f, "Internal server error: {}", msg),
|
||||
ApiError::InternalServerError {body, backend_hostname} =>
|
||||
write!(f, "Internal Server Error; backend hostname: {:?} ; body: {}; ", backend_hostname, body),
|
||||
// Server response handling errors
|
||||
ApiError::DeserializationError(error) => write!(f, "Deserialization error: {}", error),
|
||||
// Network errors
|
||||
|
||||
+7
-57
@@ -1,68 +1,18 @@
|
||||
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use crate::error::api_error::ApiError;
|
||||
use crate::error::client_error::ClientError;
|
||||
use crate::error::storyteller_error::StorytellerError;
|
||||
use crate::utils::api_host::ApiHost;
|
||||
use crate::utils::constants::{APPLICATION_JSON, USER_AGENT};
|
||||
use crate::utils::filter_bad_response::filter_bad_response;
|
||||
use crate::utils::basic_json_post_request::basic_json_post_request;
|
||||
use artcraft_api_defs::generate::object::generate_hunyuan_2_0_image_to_3d::{GenerateHunyuan20ImageTo3dRequest, GenerateHunyuan20ImageTo3dResponse, GENERATE_HUNYUAN_2_0_IMAGE_TO_3D_URL_PATH};
|
||||
use log::{debug, info};
|
||||
use reqwest::Client;
|
||||
|
||||
pub async fn generate_hunyuan3d_2_0_image_to_3d(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: GenerateHunyuan20ImageTo3dRequest,
|
||||
) -> Result<GenerateHunyuan20ImageTo3dResponse, StorytellerError> {
|
||||
|
||||
let url = get_route(api_host);
|
||||
|
||||
info!("Requesting {:?}", &url);
|
||||
|
||||
let client = Client::builder()
|
||||
.gzip(true)
|
||||
.build()
|
||||
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
|
||||
|
||||
let mut request_builder = client.post(url)
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.header("Accept", APPLICATION_JSON)
|
||||
.header("Content-Type", APPLICATION_JSON);
|
||||
|
||||
if let Some(creds) = maybe_creds {
|
||||
if let Some(header) = &creds.maybe_as_cookie_header() {
|
||||
request_builder = request_builder.header("Cookie", header);
|
||||
}
|
||||
}
|
||||
|
||||
let request_body = serde_json::to_string(&request)
|
||||
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
|
||||
|
||||
debug!("Request body: {:?}", request_body);
|
||||
|
||||
let response = request_builder
|
||||
.body(request_body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
let response = filter_bad_response(response)
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
let response_body = &response.text()
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
debug!("Response body: {:?}", response_body);
|
||||
|
||||
let response = serde_json::from_str(&response_body)
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn get_route(api_host: &ApiHost) -> String {
|
||||
let api_hostname = api_host.to_api_hostname();
|
||||
format!("https://{}{}", api_hostname, GENERATE_HUNYUAN_2_0_IMAGE_TO_3D_URL_PATH)
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
GENERATE_HUNYUAN_2_0_IMAGE_TO_3D_URL_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
|
||||
+7
-57
@@ -1,68 +1,18 @@
|
||||
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use crate::error::api_error::ApiError;
|
||||
use crate::error::client_error::ClientError;
|
||||
use crate::error::storyteller_error::StorytellerError;
|
||||
use crate::utils::api_host::ApiHost;
|
||||
use crate::utils::constants::{APPLICATION_JSON, USER_AGENT};
|
||||
use crate::utils::filter_bad_response::filter_bad_response;
|
||||
use crate::utils::basic_json_post_request::basic_json_post_request;
|
||||
use artcraft_api_defs::generate::object::generate_hunyuan_2_1_image_to_3d::{GenerateHunyuan21ImageTo3dRequest, GenerateHunyuan21ImageTo3dResponse, GENERATE_HUNYUAN_2_1_IMAGE_TO_3D_URL_PATH};
|
||||
use log::{debug, info};
|
||||
use reqwest::Client;
|
||||
|
||||
pub async fn generate_hunyuan3d_2_1_image_to_3d(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: GenerateHunyuan21ImageTo3dRequest,
|
||||
) -> Result<GenerateHunyuan21ImageTo3dResponse, StorytellerError> {
|
||||
|
||||
let url = get_route(api_host);
|
||||
|
||||
info!("Requesting {:?}", &url);
|
||||
|
||||
let client = Client::builder()
|
||||
.gzip(true)
|
||||
.build()
|
||||
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
|
||||
|
||||
let mut request_builder = client.post(url)
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.header("Accept", APPLICATION_JSON)
|
||||
.header("Content-Type", APPLICATION_JSON);
|
||||
|
||||
if let Some(creds) = maybe_creds {
|
||||
if let Some(header) = &creds.maybe_as_cookie_header() {
|
||||
request_builder = request_builder.header("Cookie", header);
|
||||
}
|
||||
}
|
||||
|
||||
let request_body = serde_json::to_string(&request)
|
||||
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
|
||||
|
||||
debug!("Request body: {:?}", request_body);
|
||||
|
||||
let response = request_builder
|
||||
.body(request_body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
let response = filter_bad_response(response)
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
let response_body = &response.text()
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
debug!("Response body: {:?}", response_body);
|
||||
|
||||
let response = serde_json::from_str(&response_body)
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn get_route(api_host: &ApiHost) -> String {
|
||||
let api_hostname = api_host.to_api_hostname();
|
||||
format!("https://{}{}", api_hostname, GENERATE_HUNYUAN_2_1_IMAGE_TO_3D_URL_PATH)
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
GENERATE_HUNYUAN_2_1_IMAGE_TO_3D_URL_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
|
||||
+7
-57
@@ -1,68 +1,18 @@
|
||||
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use crate::error::api_error::ApiError;
|
||||
use crate::error::client_error::ClientError;
|
||||
use crate::error::storyteller_error::StorytellerError;
|
||||
use crate::utils::api_host::ApiHost;
|
||||
use crate::utils::constants::{APPLICATION_JSON, USER_AGENT};
|
||||
use crate::utils::filter_bad_response::filter_bad_response;
|
||||
use crate::utils::basic_json_post_request::basic_json_post_request;
|
||||
use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::{GenerateKling16ProImageToVideoRequest, GenerateKling16ProImageToVideoResponse, GENERATE_KLING_1_6_PRO_IMAGE_TO_VIDEO_URL_PATH};
|
||||
use log::{debug, info};
|
||||
use reqwest::Client;
|
||||
|
||||
pub async fn generate_kling_16_pro_image_to_video(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: GenerateKling16ProImageToVideoRequest,
|
||||
) -> Result<GenerateKling16ProImageToVideoResponse, StorytellerError> {
|
||||
|
||||
let url = get_route(api_host);
|
||||
|
||||
info!("Requesting {:?}", &url);
|
||||
|
||||
let client = Client::builder()
|
||||
.gzip(true)
|
||||
.build()
|
||||
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
|
||||
|
||||
let mut request_builder = client.post(url)
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.header("Accept", APPLICATION_JSON)
|
||||
.header("Content-Type", APPLICATION_JSON);
|
||||
|
||||
if let Some(creds) = maybe_creds {
|
||||
if let Some(header) = &creds.maybe_as_cookie_header() {
|
||||
request_builder = request_builder.header("Cookie", header);
|
||||
}
|
||||
}
|
||||
|
||||
let request_body = serde_json::to_string(&request)
|
||||
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
|
||||
|
||||
debug!("Request body: {:?}", request_body);
|
||||
|
||||
let response = request_builder
|
||||
.body(request_body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
let response = filter_bad_response(response)
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
let response_body = &response.text()
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
debug!("Response body: {:?}", response_body);
|
||||
|
||||
let response = serde_json::from_str(&response_body)
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn get_route(api_host: &ApiHost) -> String {
|
||||
let api_hostname = api_host.to_api_hostname();
|
||||
format!("https://{}{}", api_hostname, GENERATE_KLING_1_6_PRO_IMAGE_TO_VIDEO_URL_PATH)
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
GENERATE_KLING_1_6_PRO_IMAGE_TO_VIDEO_URL_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
|
||||
+7
-57
@@ -1,68 +1,18 @@
|
||||
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use crate::error::api_error::ApiError;
|
||||
use crate::error::client_error::ClientError;
|
||||
use crate::error::storyteller_error::StorytellerError;
|
||||
use crate::utils::api_host::ApiHost;
|
||||
use crate::utils::constants::{APPLICATION_JSON, USER_AGENT};
|
||||
use crate::utils::filter_bad_response::filter_bad_response;
|
||||
use crate::utils::basic_json_post_request::basic_json_post_request;
|
||||
use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::{GenerateKling21MasterImageToVideoRequest, GenerateKling21MasterImageToVideoResponse, GENERATE_KLING_2_1_MASTER_IMAGE_TO_VIDEO_URL_PATH};
|
||||
use log::{debug, info};
|
||||
use reqwest::Client;
|
||||
|
||||
pub async fn generate_kling_21_master_image_to_video(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: GenerateKling21MasterImageToVideoRequest,
|
||||
) -> Result<GenerateKling21MasterImageToVideoResponse, StorytellerError> {
|
||||
|
||||
let url = get_route(api_host);
|
||||
|
||||
info!("Requesting {:?}", &url);
|
||||
|
||||
let client = Client::builder()
|
||||
.gzip(true)
|
||||
.build()
|
||||
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
|
||||
|
||||
let mut request_builder = client.post(url)
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.header("Accept", APPLICATION_JSON)
|
||||
.header("Content-Type", APPLICATION_JSON);
|
||||
|
||||
if let Some(creds) = maybe_creds {
|
||||
if let Some(header) = &creds.maybe_as_cookie_header() {
|
||||
request_builder = request_builder.header("Cookie", header);
|
||||
}
|
||||
}
|
||||
|
||||
let request_body = serde_json::to_string(&request)
|
||||
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
|
||||
|
||||
debug!("Request body: {:?}", request_body);
|
||||
|
||||
let response = request_builder
|
||||
.body(request_body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
let response = filter_bad_response(response)
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
let response_body = &response.text()
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
debug!("Response body: {:?}", response_body);
|
||||
|
||||
let response = serde_json::from_str(&response_body)
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn get_route(api_host: &ApiHost) -> String {
|
||||
let api_hostname = api_host.to_api_hostname();
|
||||
format!("https://{}{}", api_hostname, GENERATE_KLING_2_1_MASTER_IMAGE_TO_VIDEO_URL_PATH)
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
GENERATE_KLING_2_1_MASTER_IMAGE_TO_VIDEO_URL_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
|
||||
+7
-57
@@ -1,68 +1,18 @@
|
||||
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use crate::error::api_error::ApiError;
|
||||
use crate::error::client_error::ClientError;
|
||||
use crate::error::storyteller_error::StorytellerError;
|
||||
use crate::utils::api_host::ApiHost;
|
||||
use crate::utils::constants::{APPLICATION_JSON, USER_AGENT};
|
||||
use crate::utils::filter_bad_response::filter_bad_response;
|
||||
use crate::utils::basic_json_post_request::basic_json_post_request;
|
||||
use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::{GenerateKling21ProImageToVideoRequest, GenerateKling21ProImageToVideoResponse, GENERATE_KLING_2_1_PRO_IMAGE_TO_VIDEO_URL_PATH};
|
||||
use log::{debug, info};
|
||||
use reqwest::Client;
|
||||
|
||||
pub async fn generate_kling_21_pro_image_to_video(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: GenerateKling21ProImageToVideoRequest,
|
||||
) -> Result<GenerateKling21ProImageToVideoResponse, StorytellerError> {
|
||||
|
||||
let url = get_route(api_host);
|
||||
|
||||
info!("Requesting {:?}", &url);
|
||||
|
||||
let client = Client::builder()
|
||||
.gzip(true)
|
||||
.build()
|
||||
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
|
||||
|
||||
let mut request_builder = client.post(url)
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.header("Accept", APPLICATION_JSON)
|
||||
.header("Content-Type", APPLICATION_JSON);
|
||||
|
||||
if let Some(creds) = maybe_creds {
|
||||
if let Some(header) = &creds.maybe_as_cookie_header() {
|
||||
request_builder = request_builder.header("Cookie", header);
|
||||
}
|
||||
}
|
||||
|
||||
let request_body = serde_json::to_string(&request)
|
||||
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
|
||||
|
||||
debug!("Request body: {:?}", request_body);
|
||||
|
||||
let response = request_builder
|
||||
.body(request_body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
let response = filter_bad_response(response)
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
let response_body = &response.text()
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
debug!("Response body: {:?}", response_body);
|
||||
|
||||
let response = serde_json::from_str(&response_body)
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn get_route(api_host: &ApiHost) -> String {
|
||||
let api_hostname = api_host.to_api_hostname();
|
||||
format!("https://{}{}", api_hostname, GENERATE_KLING_2_1_PRO_IMAGE_TO_VIDEO_URL_PATH)
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
GENERATE_KLING_2_1_PRO_IMAGE_TO_VIDEO_URL_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
|
||||
+7
-57
@@ -1,68 +1,18 @@
|
||||
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use crate::error::api_error::ApiError;
|
||||
use crate::error::client_error::ClientError;
|
||||
use crate::error::storyteller_error::StorytellerError;
|
||||
use crate::utils::api_host::ApiHost;
|
||||
use crate::utils::constants::{APPLICATION_JSON, USER_AGENT};
|
||||
use crate::utils::filter_bad_response::filter_bad_response;
|
||||
use crate::utils::basic_json_post_request::basic_json_post_request;
|
||||
use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::{GenerateSeedance10LiteImageToVideoRequest, GenerateSeedance10LiteImageToVideoResponse, GENERATE_SEEDANCE_1_0_LITE_IMAGE_TO_VIDEO_URL_PATH};
|
||||
use log::{debug, info};
|
||||
use reqwest::Client;
|
||||
|
||||
pub async fn generate_seedance_1_0_lite_image_to_video(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: GenerateSeedance10LiteImageToVideoRequest,
|
||||
) -> Result<GenerateSeedance10LiteImageToVideoResponse, StorytellerError> {
|
||||
|
||||
let url = get_route(api_host);
|
||||
|
||||
info!("Requesting {:?}", &url);
|
||||
|
||||
let client = Client::builder()
|
||||
.gzip(true)
|
||||
.build()
|
||||
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
|
||||
|
||||
let mut request_builder = client.post(url)
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.header("Accept", APPLICATION_JSON)
|
||||
.header("Content-Type", APPLICATION_JSON);
|
||||
|
||||
if let Some(creds) = maybe_creds {
|
||||
if let Some(header) = &creds.maybe_as_cookie_header() {
|
||||
request_builder = request_builder.header("Cookie", header);
|
||||
}
|
||||
}
|
||||
|
||||
let request_body = serde_json::to_string(&request)
|
||||
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
|
||||
|
||||
debug!("Request body: {:?}", request_body);
|
||||
|
||||
let response = request_builder
|
||||
.body(request_body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
let response = filter_bad_response(response)
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
let response_body = &response.text()
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
debug!("Response body: {:?}", response_body);
|
||||
|
||||
let response = serde_json::from_str(&response_body)
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn get_route(api_host: &ApiHost) -> String {
|
||||
let api_hostname = api_host.to_api_hostname();
|
||||
format!("https://{}{}", api_hostname, GENERATE_SEEDANCE_1_0_LITE_IMAGE_TO_VIDEO_URL_PATH)
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
GENERATE_SEEDANCE_1_0_LITE_IMAGE_TO_VIDEO_URL_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
|
||||
+7
-57
@@ -1,68 +1,18 @@
|
||||
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use crate::error::api_error::ApiError;
|
||||
use crate::error::client_error::ClientError;
|
||||
use crate::error::storyteller_error::StorytellerError;
|
||||
use crate::utils::api_host::ApiHost;
|
||||
use crate::utils::constants::{APPLICATION_JSON, USER_AGENT};
|
||||
use crate::utils::filter_bad_response::filter_bad_response;
|
||||
use crate::utils::basic_json_post_request::basic_json_post_request;
|
||||
use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::{GenerateVeo2ImageToVideoRequest, GenerateVeo2ImageToVideoResponse, GENERATE_VEO_2_IMAGE_TO_VIDEO_URL_PATH};
|
||||
use log::{debug, info};
|
||||
use reqwest::Client;
|
||||
|
||||
pub async fn generate_veo_2_image_to_video(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: GenerateVeo2ImageToVideoRequest,
|
||||
) -> Result<GenerateVeo2ImageToVideoResponse, StorytellerError> {
|
||||
|
||||
let url = get_route(api_host);
|
||||
|
||||
info!("Requesting {:?}", &url);
|
||||
|
||||
let client = Client::builder()
|
||||
.gzip(true)
|
||||
.build()
|
||||
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
|
||||
|
||||
let mut request_builder = client.post(url)
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.header("Accept", APPLICATION_JSON)
|
||||
.header("Content-Type", APPLICATION_JSON);
|
||||
|
||||
if let Some(creds) = maybe_creds {
|
||||
if let Some(header) = &creds.maybe_as_cookie_header() {
|
||||
request_builder = request_builder.header("Cookie", header);
|
||||
}
|
||||
}
|
||||
|
||||
let request_body = serde_json::to_string(&request)
|
||||
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
|
||||
|
||||
debug!("Request body: {:?}", request_body);
|
||||
|
||||
let response = request_builder
|
||||
.body(request_body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
let response = filter_bad_response(response)
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
let response_body = &response.text()
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
debug!("Response body: {:?}", response_body);
|
||||
|
||||
let response = serde_json::from_str(&response_body)
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn get_route(api_host: &ApiHost) -> String {
|
||||
let api_hostname = api_host.to_api_hostname();
|
||||
format!("https://{}{}", api_hostname, GENERATE_VEO_2_IMAGE_TO_VIDEO_URL_PATH)
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
GENERATE_VEO_2_IMAGE_TO_VIDEO_URL_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::error::api_error::ApiError;
|
||||
use crate::utils::status_codes::*;
|
||||
use anyhow::anyhow;
|
||||
use log::error;
|
||||
|
||||
/// Pass request errors to this method for standard error handling behavior.
|
||||
/// This function needs to take temporary ownership, but will return it to the caller.
|
||||
@@ -13,6 +14,15 @@ pub async fn filter_bad_response(response: reqwest::Response) -> Result<reqwest:
|
||||
let status = response.status();
|
||||
|
||||
if !status.is_success() {
|
||||
// The host in the fleet that handled the request.
|
||||
let backend_hostname= response.headers()
|
||||
.get("x-backend-hostname")
|
||||
.map(|header| header.to_str().unwrap_or_else(|err| {
|
||||
error!("Failed to parse x-backend-hostname header: {:?}", err);
|
||||
"unknown"
|
||||
}))
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let response_body = match response.text().await {
|
||||
Ok(text) => text.to_string(),
|
||||
Err(err) => format!("Could not read response body: {:?}", err),
|
||||
@@ -23,7 +33,7 @@ pub async fn filter_bad_response(response: reqwest::Response) -> Result<reqwest:
|
||||
STATUS_403_FORBIDDEN => Err(ApiError::Forbidden(response_body)),
|
||||
STATUS_404_NOT_FOUND => Err(ApiError::NotFound(response_body)),
|
||||
STATUS_429_TOO_MANY_REQUESTS => Err(ApiError::TooManyRequests(response_body)),
|
||||
STATUS_500_INTERNAL_SERVER_ERROR => Err(ApiError::InternalServerError(response_body)),
|
||||
STATUS_500_INTERNAL_SERVER_ERROR => Err(ApiError::InternalServerError { body: response_body, backend_hostname }),
|
||||
_ => Err(ApiError::Other(anyhow!("Bad status code: {}; message: {:?}", status, response_body))),
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -41,6 +41,10 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
std::fs::create_dir_all(&cache_dir)?;
|
||||
|
||||
let path = cache_dir.join(COOKIES_FILENAME);
|
||||
|
||||
// NB(bt): Let's always log this.
|
||||
println!("Tauri plugin http: using cookie store at {}", path.display());
|
||||
|
||||
let file = File::options()
|
||||
.create(true)
|
||||
.append(true)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import {
|
||||
faGear,
|
||||
faImages,
|
||||
@@ -6,8 +6,7 @@ import {
|
||||
faFilm,
|
||||
faPaintbrush,
|
||||
faImage,
|
||||
faDiamond,
|
||||
faGem,
|
||||
// faGem,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import { AuthButtons } from "./AuthButtons";
|
||||
@@ -20,7 +19,7 @@ import {
|
||||
} from "@storyteller/ui-gallery-modal";
|
||||
import { SettingsModal } from "@storyteller/ui-settings-modal";
|
||||
import { Tooltip } from "@storyteller/ui-tooltip";
|
||||
import { downloadFileFromUrl } from "@storyteller/api";
|
||||
import { downloadFileFromUrl, FilterMediaClasses } from "@storyteller/api";
|
||||
import {
|
||||
MenuIconSelector,
|
||||
MenuIconItem,
|
||||
@@ -36,12 +35,15 @@ import {
|
||||
is3DSceneLoaded,
|
||||
set3DPageMounted,
|
||||
} from "~/pages/PageEnigma/Editor/editor";
|
||||
import { usePricingModalStore } from "@storyteller/ui-pricing-modal";
|
||||
// import { usePricingModalStore } from "@storyteller/ui-pricing-modal"; - Uncomment for pricing modal - BFlat
|
||||
import toast from "react-hot-toast";
|
||||
interface Props {
|
||||
pageName: string;
|
||||
loginSignUpPressed: () => void;
|
||||
}
|
||||
|
||||
const SWITCHER_THROTTLE_TIME = 500; // milliseconds
|
||||
|
||||
const appMenuTabs: MenuIconItem[] = [
|
||||
{
|
||||
id: "2D",
|
||||
@@ -74,7 +76,7 @@ const appMenuTabs: MenuIconItem[] = [
|
||||
export const topNavMediaId = signal<string>("");
|
||||
export const topNavMediaUrl = signal<string>("");
|
||||
|
||||
export const TopBar = ({ pageName,loginSignUpPressed }: Props) => {
|
||||
export const TopBar = ({ pageName, loginSignUpPressed }: Props) => {
|
||||
useSignals();
|
||||
|
||||
const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);
|
||||
@@ -88,15 +90,32 @@ export const TopBar = ({ pageName,loginSignUpPressed }: Props) => {
|
||||
|
||||
const is3DSceneReady = is3DSceneLoaded.value;
|
||||
const is3DEditorReady = is3DEditorInitialized.value;
|
||||
const [disableSwitcher, setDisableSwitcher] = useState(false);
|
||||
const switcherThrottle = useRef(false);
|
||||
|
||||
const disableTabSwitcher = () => {
|
||||
return (
|
||||
useTabStore.getState().activeTabId === "3D" &&
|
||||
!is3DEditorReady &&
|
||||
!is3DSceneReady
|
||||
disableSwitcher ||
|
||||
(useTabStore.getState().activeTabId === "3D" &&
|
||||
!is3DEditorReady &&
|
||||
!is3DSceneReady)
|
||||
);
|
||||
};
|
||||
|
||||
const { toggleModal } = usePricingModalStore();
|
||||
const downloadFile = async (url: string, mediaClass?: string) => {
|
||||
try {
|
||||
await downloadFileFromUrl(url);
|
||||
if (mediaClass === FilterMediaClasses.DIMENSIONAL) {
|
||||
toast.success(`Downloaded 3D model`);
|
||||
} else {
|
||||
toast.success(`Downloaded ${mediaClass}`);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("Failed to download file");
|
||||
}
|
||||
};
|
||||
|
||||
// const { toggleModal } = usePricingModalStore(); - Uncomment for pricing modal - BFlat
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -119,6 +138,13 @@ export const TopBar = ({ pageName,loginSignUpPressed }: Props) => {
|
||||
activeMenu={tabStore.activeTabId}
|
||||
disabled={disableTabSwitcher()}
|
||||
onMenuChange={(tabId) => {
|
||||
// Prevent a second input if the switcher is throttled.
|
||||
if (switcherThrottle.current) {
|
||||
return;
|
||||
}
|
||||
switcherThrottle.current = true;
|
||||
setDisableSwitcher(true);
|
||||
|
||||
// Disable 3d engine to prevent memory leak.
|
||||
if (tabId === "3D") {
|
||||
set3DPageMounted(true);
|
||||
@@ -126,6 +152,12 @@ export const TopBar = ({ pageName,loginSignUpPressed }: Props) => {
|
||||
set3DPageMounted(false);
|
||||
}
|
||||
useTabStore.getState().setActiveTab(tabId);
|
||||
setTimeout(() => {
|
||||
// Clear the throttle
|
||||
switcherThrottle.current = false;
|
||||
// Trigger a new re-render (important)
|
||||
setDisableSwitcher(false);
|
||||
}, SWITCHER_THROTTLE_TIME);
|
||||
}}
|
||||
className="w-fit"
|
||||
/>
|
||||
@@ -145,8 +177,9 @@ export const TopBar = ({ pageName,loginSignUpPressed }: Props) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3.5">
|
||||
<div className="flex justify-end gap-3.5 pr-2">
|
||||
<div className="flex gap-2">
|
||||
{/* - Uncomment for pricing modal - BFlat */}
|
||||
{/* <Button
|
||||
variant="primary"
|
||||
icon={faGem}
|
||||
@@ -173,9 +206,7 @@ export const TopBar = ({ pageName,loginSignUpPressed }: Props) => {
|
||||
|
||||
<Activity />
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<AuthButtons loginSignUpPressed={loginSignUpPressed}/>
|
||||
</div>
|
||||
<AuthButtons loginSignUpPressed={loginSignUpPressed} />
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
@@ -186,7 +217,7 @@ export const TopBar = ({ pageName,loginSignUpPressed }: Props) => {
|
||||
globalAccountLogoutCallback={() => setLogoutStates()}
|
||||
/>
|
||||
|
||||
<GalleryModal mode="view" onDownloadClicked={downloadFileFromUrl} />
|
||||
<GalleryModal mode="view" onDownloadClicked={downloadFile} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1216,10 +1216,10 @@ class Editor {
|
||||
: this.render_camera_aspect_ratio === meraAspectRatio.VERTICAL_9_16
|
||||
? 576
|
||||
: this.render_camera_aspect_ratio ===
|
||||
meraAspectRatio.HORIZONTAL_3_2
|
||||
meraAspectRatio.HORIZONTAL_3_2
|
||||
? 900
|
||||
: this.render_camera_aspect_ratio ===
|
||||
meraAspectRatio.VERTICAL_2_3
|
||||
meraAspectRatio.VERTICAL_2_3
|
||||
? 600
|
||||
: 1000;
|
||||
const height =
|
||||
@@ -1228,10 +1228,10 @@ class Editor {
|
||||
: this.render_camera_aspect_ratio === meraAspectRatio.VERTICAL_9_16
|
||||
? 1024
|
||||
: this.render_camera_aspect_ratio ===
|
||||
meraAspectRatio.HORIZONTAL_3_2
|
||||
meraAspectRatio.HORIZONTAL_3_2
|
||||
? 600
|
||||
: this.render_camera_aspect_ratio ===
|
||||
meraAspectRatio.VERTICAL_2_3
|
||||
meraAspectRatio.VERTICAL_2_3
|
||||
? 900
|
||||
: 1000;
|
||||
|
||||
@@ -1323,7 +1323,7 @@ class Editor {
|
||||
) {
|
||||
this.composer.render();
|
||||
//this.rawRenderer.render(this.activeScene.scene, this.render_camera!);
|
||||
this.render_composer.render();
|
||||
// this.render_composer.render();
|
||||
} else if (this.renderer && this.render_camera && !this.rendering) {
|
||||
this.renderer.setSize(this.render_width, this.render_height);
|
||||
this.renderer.render(this.activeScene.scene, this.render_camera);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import React, { useContext, useEffect } from "react";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { TopBar } from "~/components";
|
||||
import { Controls3D } from "./comps/Controls3D";
|
||||
@@ -73,26 +73,32 @@ import {
|
||||
// videoGenerationModels,
|
||||
// useModelSelectorStore,
|
||||
} from "@storyteller/ui-model-selector";
|
||||
import { LoginModal } from "@storyteller/ui-login-modal";
|
||||
import { LoginModal, useLoginModalStore } from "@storyteller/ui-login-modal";
|
||||
import PageDraw from "../PageDraw/PageDraw";
|
||||
import { useTabStore } from "../Stores/TabState";
|
||||
|
||||
export const PageEditor = () => {
|
||||
useSignals();
|
||||
const [isLoginModalOpen, setIsLoginModalOpen] = useState<boolean>(false);
|
||||
|
||||
const { triggerRecheck } = useLoginModalStore();
|
||||
|
||||
const { status } = authentication;
|
||||
useEffect(() => {
|
||||
if (status.value !== AUTH_STATUS.LOGGED_OUT) {
|
||||
setIsLoginModalOpen(true);
|
||||
if (status.value === AUTH_STATUS.LOGGED_OUT) {
|
||||
triggerRecheck();
|
||||
}
|
||||
}, [status.value]);
|
||||
|
||||
}, [status.value, triggerRecheck]);
|
||||
|
||||
const tabStore = useTabStore();
|
||||
const handleOverlayClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
|
||||
// TODO: For 3d Promptbox to accept this later on
|
||||
// const { selectedModels } = useModelSelectorStore();
|
||||
// const selectedModel =
|
||||
// selectedModels[ModelCategory.Editor3D] ||
|
||||
// videoGenerationModels[0]?.label;
|
||||
|
||||
useEffect(() => {
|
||||
timelineHeight.value = 0; //timelineHeight.value = 208;
|
||||
sidePanelWidth.value = 340;
|
||||
@@ -409,30 +415,30 @@ export const PageEditor = () => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [tabStore.activeTabId, editorEngine]);
|
||||
|
||||
|
||||
return (
|
||||
<div className="w-screen">
|
||||
<TopBar loginSignUpPressed={()=> {
|
||||
setIsLoginModalOpen(true)
|
||||
}} pageName="Edit Scene" />
|
||||
{isLoginModalOpen && (
|
||||
<LoginModal
|
||||
onClose={() => setIsLoginModalOpen(false)}
|
||||
videoSrc2D="/resources/videos/artcraft-canvas-demo.mp4"
|
||||
videoSrc3D="/resources/videos/artcraft-3d-demo.mp4"
|
||||
onOpenChange={(isOpen: boolean) => {
|
||||
if (isOpen) {
|
||||
disableHotkeyInput(DomLevels.DIALOGUE);
|
||||
} else {
|
||||
enableHotkeyInput(DomLevels.DIALOGUE);
|
||||
}
|
||||
}}
|
||||
onArtCraftAuthSuccess={(userInfo: any) => {
|
||||
authentication.status.value = AUTH_STATUS.LOGGED_IN;
|
||||
authentication.userInfo.value = userInfo;
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<TopBar
|
||||
loginSignUpPressed={() => {
|
||||
console.log("PRESSED");
|
||||
triggerRecheck();
|
||||
}}
|
||||
pageName="Edit Scene"
|
||||
/>
|
||||
<LoginModal
|
||||
videoSrc2D="/resources/videos/artcraft-canvas-demo.mp4"
|
||||
videoSrc3D="/resources/videos/artcraft-3d-demo.mp4"
|
||||
onOpenChange={(isOpen: boolean) => {
|
||||
if (isOpen) {
|
||||
disableHotkeyInput(DomLevels.DIALOGUE);
|
||||
} else {
|
||||
enableHotkeyInput(DomLevels.DIALOGUE);
|
||||
}
|
||||
}}
|
||||
onArtCraftAuthSuccess={(userInfo: any) => {
|
||||
authentication.status.value = AUTH_STATUS.LOGGED_IN;
|
||||
authentication.userInfo.value = userInfo;
|
||||
}}
|
||||
/>
|
||||
{tabStore.activeTabId == "3D" && (
|
||||
<div>
|
||||
<OnboardingHelper />
|
||||
@@ -565,8 +571,6 @@ export const PageEditor = () => {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -13,14 +13,19 @@ 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) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const { selectedModels } = useModelSelectorStore();
|
||||
|
||||
const selectedModel =
|
||||
selectedModels[ModelCategory.TextToImage] ||
|
||||
imageGenerationModels[0]?.label;
|
||||
|
||||
const selectedModelInfo : ModelInfo | undefined = IMAGE_MODELS_BY_LABEL[selectedModel];
|
||||
|
||||
const jobContext: JobContextType = {
|
||||
jobTokens: [],
|
||||
addJobToken: () => {},
|
||||
@@ -48,6 +53,7 @@ const TextToImage = ({ imageMediaId, imageUrl }: TextToImageProps) => {
|
||||
return jobContext;
|
||||
}}
|
||||
model={selectedModel}
|
||||
modelInfo={selectedModelInfo}
|
||||
imageMediaId={imageMediaId}
|
||||
url={imageUrl ?? undefined}
|
||||
onEnqueuePressed={async () => {
|
||||
|
||||
@@ -8,6 +8,8 @@ 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 {
|
||||
imageMediaId?: string;
|
||||
@@ -17,13 +19,12 @@ interface ImageToVideoProps {
|
||||
const ImageToVideo = ({ imageMediaId, imageUrl }: ImageToVideoProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const { selectedModels } = useModelSelectorStore();
|
||||
|
||||
const selectedModel =
|
||||
selectedModels[ModelCategory.ImageToVideo] ||
|
||||
videoGenerationModels[0]?.label;
|
||||
|
||||
console.log("Selected model:", selectedModel)
|
||||
console.log("Image Media Id:", imageMediaId)
|
||||
console.log("Image URL:", imageUrl)
|
||||
const selectedModelInfo : ModelInfo | undefined = VIDEO_MODELS_BY_LABEL[selectedModel];
|
||||
|
||||
const jobContext: JobContextType = {
|
||||
jobTokens: [],
|
||||
@@ -53,6 +54,7 @@ const ImageToVideo = ({ imageMediaId, imageUrl }: ImageToVideoProps) => {
|
||||
return jobContext;
|
||||
}}
|
||||
model={selectedModel}
|
||||
modelInfo={selectedModelInfo}
|
||||
imageMediaId={imageMediaId}
|
||||
url={imageUrl ?? undefined}
|
||||
onEnqueuePressed={async () => {
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
|
||||
export * from './lib/BuildEnv';
|
||||
export * from './lib/BuildEnvType';
|
||||
export * from './lib/build-env';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/// <reference types='vitest' />
|
||||
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import dts from 'vite-plugin-dts';
|
||||
@@ -7,7 +7,7 @@ import * as path from 'path';
|
||||
export default defineConfig(() => ({
|
||||
root: __dirname,
|
||||
cacheDir: '../../node_modules/.vite/libs/build-env',
|
||||
plugins: [react(), dts({ entryRoot: 'src', tsconfigPath: path.join(__dirname, 'tsconfig.lib.json') })],
|
||||
plugins: [react(), dts({ entryRoot: 'src', tsconfigPath: path.join(__dirname, 'tsconfig.lib.json') }), ],
|
||||
// Uncomment this if you are using workers.
|
||||
// worker: {
|
||||
// plugins: [ nxViteTsPaths() ],
|
||||
@@ -15,35 +15,33 @@ export default defineConfig(() => ({
|
||||
// Configuration for building your library.
|
||||
// See: https://vitejs.dev/guide/build.html#library-mode
|
||||
build: {
|
||||
outDir: './dist',
|
||||
emptyOutDir: true,
|
||||
reportCompressedSize: true,
|
||||
commonjsOptions: {
|
||||
transformMixedEsModules: true,
|
||||
},
|
||||
lib: {
|
||||
// Could also be a dictionary or array of multiple entry points.
|
||||
entry: 'src/index.ts',
|
||||
name: '@frontend/build-env',
|
||||
fileName: 'index',
|
||||
// Change this to the formats you want to support.
|
||||
// Don't forget to update your package.json as well.
|
||||
formats: ['es' as const]
|
||||
},
|
||||
rollupOptions: {
|
||||
// External packages that should not be bundled into your library.
|
||||
external: ['react','react-dom','react/jsx-runtime']
|
||||
'emptyOutDir': true,
|
||||
'transformMixedEsModules': true,
|
||||
'entry': 'src/index.ts',
|
||||
'name': '@frontend/build-env',
|
||||
'fileName': 'index',
|
||||
'formats': ['es' as const],
|
||||
'external': ['react','react-dom','react/jsx-runtime'],
|
||||
'lib': {
|
||||
'entry': "src/index.ts",
|
||||
'name': "@frontend/build-env",
|
||||
'fileName': "index",
|
||||
'formats': ['es' as const],
|
||||
},
|
||||
'rollupOptions': {"external":["'react'","'react-dom'","'react/jsx-runtime'"]},
|
||||
'outDir': "./dist",
|
||||
'reportCompressedSize': true,
|
||||
'commonjsOptions': {"transformMixedEsModules":true},
|
||||
},
|
||||
test: {
|
||||
watch: false,
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
||||
reporters: ['default'],
|
||||
coverage: {
|
||||
reportsDirectory: './test-output/vitest/coverage',
|
||||
provider: 'v8' as const,
|
||||
}
|
||||
'watch': false,
|
||||
'globals': true,
|
||||
'environment': "jsdom",
|
||||
'include': ["src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
|
||||
'reporters': ["default"],
|
||||
'coverage': {
|
||||
'reportsDirectory': './test-output/vitest/coverage',
|
||||
'provider': 'v8' as const,
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -62,7 +62,7 @@ interface GalleryModalProps {
|
||||
onSelectItem?: (id: string) => void;
|
||||
maxSelections?: number;
|
||||
onUseSelected?: (selectedItems: GalleryItem[]) => void;
|
||||
onDownloadClicked?: (url: string) => Promise<void>;
|
||||
onDownloadClicked?: (url: string, mediaClass?: string) => Promise<void>;
|
||||
onAddToSceneClicked?: (
|
||||
url: string,
|
||||
media_id: string | undefined
|
||||
|
||||
@@ -21,7 +21,7 @@ interface LightboxModalProps {
|
||||
additionalInfo?: React.ReactNode;
|
||||
downloadUrl?: string;
|
||||
mediaId?: string;
|
||||
onDownloadClicked?: (url: string) => Promise<void>;
|
||||
onDownloadClicked?: (url: string, mediaClass?: string) => Promise<void>;
|
||||
onAddToSceneClicked?: (
|
||||
url: string,
|
||||
media_id: string | undefined
|
||||
@@ -164,7 +164,7 @@ export function LightboxModal({
|
||||
icon={faDownToLine}
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
await onDownloadClicked(downloadUrl);
|
||||
await onDownloadClicked(downloadUrl, mediaClass);
|
||||
}}
|
||||
>
|
||||
Download
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
|
||||
export * from './lib/login-modal';
|
||||
export * from "./lib/login-modal";
|
||||
export * from "./lib/useLoginModalStore";
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import { Transition, TransitionChild } from "@headlessui/react";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import {
|
||||
faArrowRight,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faArrowRight } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faDiscord } from "@fortawesome/free-brands-svg-icons";
|
||||
import {
|
||||
CheckSoraSession,
|
||||
@@ -12,8 +10,9 @@ import {
|
||||
} from "@storyteller/tauri-api";
|
||||
import { ArtCraftSignUp } from "./artcraft-signup";
|
||||
import { UsersApi } from "@storyteller/api";
|
||||
import { useLoginModalStore } from "./useLoginModalStore";
|
||||
interface LoginModalProps {
|
||||
onClose: () => void;
|
||||
onClose?: () => void;
|
||||
videoSrc2D?: string;
|
||||
videoSrc3D?: string;
|
||||
onOpenChange?: (isOpen: boolean) => void;
|
||||
@@ -29,9 +28,9 @@ export function LoginModal({
|
||||
onArtCraftAuthSuccess,
|
||||
isSignUp: initialIsSignUp = true,
|
||||
}: LoginModalProps) {
|
||||
const { isOpen, recheckTrigger, closeModal } = useLoginModalStore();
|
||||
const [step, setStep] = useState(1);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
const [isLoggedInArtCraft, setIsLoggedInArtCraft] = useState(false);
|
||||
const [isSignUp, setIsSignUp] = useState(initialIsSignUp);
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
@@ -61,26 +60,35 @@ export function LoginModal({
|
||||
setStep(4); // Always go to the final step after Sora login
|
||||
});
|
||||
|
||||
// Check session on component mount
|
||||
// Check session on component mount and when recheckTrigger changes
|
||||
useEffect(() => {
|
||||
checkArtCraftLogin().then((loggedIn) => {
|
||||
if (loggedIn) {
|
||||
setIsLoggedInArtCraft(true);
|
||||
setIsOpen(false);
|
||||
closeModal();
|
||||
} else {
|
||||
setIsOpen(true);
|
||||
setStep(1); // Always start at step 1
|
||||
// Reset all modal state to initial values
|
||||
setStep(1);
|
||||
setIsLoading(false);
|
||||
setIsSignUp(initialIsSignUp);
|
||||
setErrorMessage("");
|
||||
setShowDiscord(false);
|
||||
setShowSuccess(false);
|
||||
setIsLoggedInArtCraft(false);
|
||||
|
||||
const { openModal } = useLoginModalStore.getState();
|
||||
openModal();
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
}, [recheckTrigger, closeModal, initialIsSignUp]);
|
||||
|
||||
useEffect(() => {
|
||||
if (onOpenChange) onOpenChange(isOpen);
|
||||
}, [isOpen, onOpenChange]);
|
||||
|
||||
const handleClose = () => {
|
||||
setIsOpen(false);
|
||||
onClose();
|
||||
closeModal();
|
||||
onClose?.();
|
||||
};
|
||||
|
||||
const handleNext = async () => {
|
||||
@@ -149,7 +157,8 @@ export function LoginModal({
|
||||
Join Our Community
|
||||
</h2>
|
||||
<p className="text-white/70 mb-6 text-center px-24">
|
||||
Connect with other creators, share your work, and get the latest updates in our Discord community.
|
||||
Connect with other creators, share your work, and get the latest
|
||||
updates in our Discord community.
|
||||
</p>
|
||||
<div className="flex gap-4">
|
||||
<Button
|
||||
@@ -234,15 +243,15 @@ export function LoginModal({
|
||||
try {
|
||||
let signupResponse, loginResponse;
|
||||
if (isSignUp) {
|
||||
console.log("Sign up!")
|
||||
console.log("Sign up!");
|
||||
signupResponse = await usersApi.Signup({
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
passwordConfirmation,
|
||||
});
|
||||
console.log(signupResponse)
|
||||
|
||||
console.log(signupResponse);
|
||||
|
||||
if (!signupResponse.success) {
|
||||
setErrorMessage(
|
||||
signupResponse.errorMessage ||
|
||||
@@ -251,21 +260,21 @@ export function LoginModal({
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
console.log("Path 1")
|
||||
console.log("Path 1");
|
||||
loginResponse = await usersApi.Login({
|
||||
usernameOrEmail: username || email,
|
||||
password,
|
||||
});
|
||||
console.log(loginResponse)
|
||||
console.log(loginResponse);
|
||||
} else {
|
||||
console.log("Path 2")
|
||||
console.log("Path 2");
|
||||
loginResponse = await usersApi.Login({
|
||||
usernameOrEmail: username || email,
|
||||
password,
|
||||
});
|
||||
console.log(loginResponse)
|
||||
console.log(loginResponse);
|
||||
}
|
||||
|
||||
|
||||
if (!loginResponse.success) {
|
||||
setErrorMessage(
|
||||
loginResponse.errorMessage ||
|
||||
@@ -276,12 +285,12 @@ export function LoginModal({
|
||||
}
|
||||
|
||||
setIsLoggedInArtCraft(true);
|
||||
console.log("ARTCRAFT LOGIN SUCCeSS")
|
||||
console.log("ARTCRAFT LOGIN SUCCeSS");
|
||||
if (onArtCraftAuthSuccess) {
|
||||
const session = await usersApi.GetSession();
|
||||
const userInfo = session.data?.user;
|
||||
console.log("USERINFO")
|
||||
console.log(userInfo)
|
||||
console.log("USERINFO");
|
||||
console.log(userInfo);
|
||||
if (userInfo) onArtCraftAuthSuccess(userInfo);
|
||||
}
|
||||
setShowDiscord(true); // Show Discord step after successful login
|
||||
@@ -313,11 +322,7 @@ export function LoginModal({
|
||||
return (
|
||||
<div className="flex items-end justify-center gap-2.5 mt-10 grow">
|
||||
{step === 2 && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleBack}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Button variant="secondary" onClick={handleBack} disabled={isLoading}>
|
||||
Back
|
||||
</Button>
|
||||
)}
|
||||
@@ -334,7 +339,8 @@ export function LoginModal({
|
||||
);
|
||||
};
|
||||
|
||||
return <Transition appear show={isOpen}>
|
||||
return (
|
||||
<Transition appear show={isOpen}>
|
||||
<div className="fixed inset-0 z-[100]">
|
||||
<TransitionChild
|
||||
enter="ease-out duration-300"
|
||||
@@ -376,6 +382,7 @@ export function LoginModal({
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
|
||||
export default LoginModal;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { create } from "zustand";
|
||||
|
||||
interface LoginModalStore {
|
||||
isOpen: boolean;
|
||||
recheckTrigger: number;
|
||||
openModal: () => void;
|
||||
closeModal: () => void;
|
||||
triggerRecheck: () => void;
|
||||
}
|
||||
|
||||
export const useLoginModalStore = create<LoginModalStore>((set) => ({
|
||||
isOpen: false,
|
||||
recheckTrigger: 0,
|
||||
openModal: () => set({ isOpen: true }),
|
||||
closeModal: () => set({ isOpen: false }),
|
||||
triggerRecheck: () =>
|
||||
set((state) => ({ recheckTrigger: state.recheckTrigger + 1 })),
|
||||
}));
|
||||
@@ -25,6 +25,7 @@ import { DomLevels } from "@storyteller/common";
|
||||
declare global {
|
||||
interface Window {
|
||||
__inertStripperInstalled?: boolean;
|
||||
__modalEscListenerInstalled?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +59,56 @@ if (!window.__inertStripperInstalled) {
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Modal ESC handling & stack tracking
|
||||
// If several modals are stacked, Esc will dismiss the one visually on top (highest z-index).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface ModalRegistryEntry {
|
||||
id: number;
|
||||
zIndex: number;
|
||||
onClose: () => void;
|
||||
closeOnEsc: boolean;
|
||||
}
|
||||
|
||||
const modalRegistry: ModalRegistryEntry[] = [];
|
||||
|
||||
const registerModal = (entry: ModalRegistryEntry) => {
|
||||
modalRegistry.push(entry);
|
||||
};
|
||||
|
||||
const unregisterModal = (id: number) => {
|
||||
const idx = modalRegistry.findIndex((m) => m.id === id);
|
||||
if (idx !== -1) modalRegistry.splice(idx, 1);
|
||||
};
|
||||
|
||||
const updateModal = (
|
||||
id: number,
|
||||
data: Partial<{ zIndex: number; onClose: () => void; closeOnEsc: boolean }>
|
||||
) => {
|
||||
const entry = modalRegistry.find((m) => m.id === id);
|
||||
if (entry) Object.assign(entry, data);
|
||||
};
|
||||
|
||||
const getTopEscapableModal = (): ModalRegistryEntry | undefined => {
|
||||
return modalRegistry
|
||||
.filter((m) => m.closeOnEsc)
|
||||
.sort((a, b) => b.zIndex - a.zIndex)[0];
|
||||
};
|
||||
|
||||
// Install one global ESC listener (once per page load)
|
||||
if (typeof window !== "undefined" && !window.__modalEscListenerInstalled) {
|
||||
window.__modalEscListenerInstalled = true;
|
||||
window.addEventListener("keydown", (e: KeyboardEvent) => {
|
||||
if (e.key !== "Escape" && e.key !== "Esc") return;
|
||||
const top = getTopEscapableModal();
|
||||
if (top) {
|
||||
e.preventDefault();
|
||||
top.onClose();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Simple global z-index tracker for stacked modals
|
||||
let modalZCounter = 70;
|
||||
|
||||
@@ -176,6 +227,7 @@ export const Modal = ({
|
||||
resizable = false,
|
||||
initialPosition,
|
||||
closeOnOutsideClick = true,
|
||||
closeOnEsc = true,
|
||||
allowBackgroundInteraction = false,
|
||||
expandable = false,
|
||||
}: {
|
||||
@@ -203,6 +255,10 @@ export const Modal = ({
|
||||
* If false, clicking the backdrop will NOT close the modal
|
||||
*/
|
||||
closeOnOutsideClick?: boolean;
|
||||
/**
|
||||
* If true, pressing the Escape key will close the modal
|
||||
*/
|
||||
closeOnEsc?: boolean;
|
||||
/**
|
||||
* If true, allow interacting with background (removes pointer events from backdrop)
|
||||
*/
|
||||
@@ -747,6 +803,31 @@ export const Modal = ({
|
||||
};
|
||||
}, [allowBackgroundInteraction]);
|
||||
|
||||
// Register this modal in the global registry for ESC handling
|
||||
const idRef = useRef<number>(Math.floor(Math.random() * 1e9));
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
registerModal({
|
||||
id: idRef.current,
|
||||
zIndex,
|
||||
onClose,
|
||||
closeOnEsc,
|
||||
});
|
||||
|
||||
return () => {
|
||||
unregisterModal(idRef.current);
|
||||
};
|
||||
// We intentionally want to run this only on mount/unmount when isOpen true
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isOpen]);
|
||||
|
||||
// Keep registry entry updated when props change
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
updateModal(idRef.current, { zIndex, onClose, closeOnEsc });
|
||||
}, [isOpen, zIndex, onClose, closeOnEsc]);
|
||||
|
||||
return (
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog
|
||||
|
||||
@@ -1,41 +1,37 @@
|
||||
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 } from "@storyteller/model-list";
|
||||
|
||||
export type ModelList = Omit<PopoverItem, "selected">[];
|
||||
|
||||
export const allModels = {
|
||||
video: [
|
||||
{
|
||||
id: "kling_1.6_pro",
|
||||
label: "Kling 1.6 Pro",
|
||||
icon: <FontAwesomeIcon icon={faFilm} className="h-4 w-4" />,
|
||||
description: "Good quality model",
|
||||
badges: [{ label: "2 min.", icon: <FontAwesomeIcon icon={faClock} /> }],
|
||||
},
|
||||
{
|
||||
id: "kling_2.1_pro",
|
||||
label: "Kling 2.1 Pro",
|
||||
icon: <FontAwesomeIcon icon={faFilm} className="h-4 w-4" />,
|
||||
description: "High quality model",
|
||||
badges: [{ label: "2 min.", icon: <FontAwesomeIcon icon={faClock} /> }],
|
||||
},
|
||||
{
|
||||
id: "kling_2.1_master",
|
||||
label: "Kling 2.1 Master",
|
||||
icon: <FontAwesomeIcon icon={faFilm} className="h-4 w-4" />,
|
||||
description: "Master quality model ($$)",
|
||||
badges: [{ label: "2 min.", icon: <FontAwesomeIcon icon={faClock} /> }],
|
||||
},
|
||||
{
|
||||
id: "seedance_1.0_lite",
|
||||
label: "Seedance 1.0 Lite",
|
||||
icon: <FontAwesomeIcon icon={faFilm} className="h-4 w-4" />,
|
||||
description: "Fast and high-quality model",
|
||||
badges: [{ label: "2 min.", icon: <FontAwesomeIcon icon={faClock} /> }],
|
||||
},
|
||||
{
|
||||
id: "veo_2",
|
||||
label: "Google Veo 2",
|
||||
icon: <FontAwesomeIcon icon={faFilm} className="h-4 w-4" />,
|
||||
description: "Fast and high-quality model",
|
||||
@@ -44,33 +40,54 @@ export const allModels = {
|
||||
],
|
||||
image: [
|
||||
{
|
||||
id: "todo",
|
||||
label: "Flux Pro 1.1 Ultra",
|
||||
icon: <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: <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: <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: <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: <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: "Flux Pro Ultra",
|
||||
icon: <FontAwesomeIcon icon={faImage} className="h-4 w-4" />,
|
||||
description: "High quality model",
|
||||
badges: [{ label: "15 sec.", icon: <FontAwesomeIcon icon={faClock} /> }],
|
||||
},
|
||||
{
|
||||
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} /> }],
|
||||
},
|
||||
//{
|
||||
// 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} /> }],
|
||||
//},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
{
|
||||
"path": "../popover"
|
||||
},
|
||||
{
|
||||
"path": "../../model-list"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
|
||||
@@ -45,6 +45,9 @@
|
||||
"references": [
|
||||
{
|
||||
"path": "../popover/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../../model-list/tsconfig.lib.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
faChevronUp,
|
||||
faCircleCheck,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { ModelInfo } from "@storyteller/model-list";
|
||||
|
||||
export interface PopoverItem {
|
||||
label: string;
|
||||
@@ -25,6 +26,7 @@ export interface PopoverItem {
|
||||
label: string;
|
||||
icon?: ReactNode;
|
||||
}>;
|
||||
modelInfo?: ModelInfo;
|
||||
}
|
||||
|
||||
interface PopoverMenuProps {
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
{
|
||||
"path": "../button"
|
||||
},
|
||||
{
|
||||
"path": "../../model-list"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
|
||||
@@ -45,6 +45,9 @@
|
||||
"references": [
|
||||
{
|
||||
"path": "../button/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../../model-list/tsconfig.lib.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { IsDesktopApp } from "@storyteller/tauri-utils";
|
||||
import { GalleryItem, GalleryModal } from "@storyteller/ui-gallery-modal";
|
||||
import { ModelInfo } from "@storyteller/model-list";
|
||||
|
||||
interface ReferenceImage {
|
||||
id: string;
|
||||
@@ -39,6 +40,7 @@ interface PromptBoxImageProps {
|
||||
useJobContext: () => JobContextType;
|
||||
onEnqueuePressed?: () => void | Promise<void>;
|
||||
model: string;
|
||||
modelInfo?: ModelInfo;
|
||||
imageMediaId?: string;
|
||||
url?: string;
|
||||
}
|
||||
@@ -47,6 +49,7 @@ export const PromptBoxImage = ({
|
||||
useJobContext,
|
||||
onEnqueuePressed,
|
||||
model,
|
||||
modelInfo,
|
||||
imageMediaId,
|
||||
url,
|
||||
}: PromptBoxImageProps) => {
|
||||
@@ -65,11 +68,8 @@ export const PromptBoxImage = ({
|
||||
}
|
||||
}, [imageMediaId, url]);
|
||||
|
||||
console.log("Is this a desktop app?", IsDesktopApp());
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [content, setContent] = useState("");
|
||||
//const { jobTokens, addJobToken, removeJobToken, clearJobTokens } =
|
||||
// useJobContext();
|
||||
|
||||
const [prompt, setPrompt] = useState("");
|
||||
const [isEnqueueing, setIsEnqueueing] = useState(false);
|
||||
@@ -196,7 +196,7 @@ export const PromptBoxImage = ({
|
||||
const handleEnqueue = async () => {
|
||||
setIsEnqueueing(true);
|
||||
|
||||
console.log("Prompting");
|
||||
console.log("PromptBoxImage - Prompting with model", modelInfo);
|
||||
|
||||
setTimeout(() => {
|
||||
// TODO(bt,2025-05-08): This is a hack so we don't accidentally wind up with a permanently disabled prompt box if
|
||||
@@ -205,14 +205,12 @@ export const PromptBoxImage = ({
|
||||
setIsEnqueueing(false);
|
||||
}, 10000);
|
||||
|
||||
const modelName = getModelByName(model);
|
||||
|
||||
const generateResponse = await EnqueueTextToImage({
|
||||
prompt: prompt,
|
||||
model: modelName,
|
||||
model: modelInfo,
|
||||
});
|
||||
|
||||
console.log("generateResponse", generateResponse);
|
||||
console.log("PromptBoxImage - generateResponse", generateResponse);
|
||||
|
||||
onEnqueuePressed?.();
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { PopoverMenu, PopoverItem } from "@storyteller/ui-popover";
|
||||
import { Tooltip } from "@storyteller/ui-tooltip";
|
||||
import { Button, ToggleButton } from "@storyteller/ui-button";
|
||||
import { Modal } from "@storyteller/ui-modal";
|
||||
import { FalKlingImageToVideo } from "@storyteller/tauri-api";
|
||||
import { EnqueueImageToVideo } from "@storyteller/tauri-api";
|
||||
import {
|
||||
faMessageXmark,
|
||||
faMessageCheck,
|
||||
@@ -20,6 +20,7 @@ import { faRectangle } from "@fortawesome/pro-regular-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { IsDesktopApp } from "@storyteller/tauri-utils";
|
||||
import { GalleryItem, GalleryModal } from "@storyteller/ui-gallery-modal";
|
||||
import { ModelInfo } from "@storyteller/model-list";
|
||||
|
||||
interface ReferenceImage {
|
||||
id: string;
|
||||
@@ -32,6 +33,7 @@ interface PromptBoxVideoProps {
|
||||
useJobContext: () => JobContextType;
|
||||
onEnqueuePressed?: () => void | Promise<void>;
|
||||
model: string;
|
||||
modelInfo?: ModelInfo;
|
||||
imageMediaId?: string;
|
||||
url?: string;
|
||||
}
|
||||
@@ -40,6 +42,7 @@ export const PromptBoxVideo = ({
|
||||
useJobContext,
|
||||
onEnqueuePressed,
|
||||
model,
|
||||
modelInfo,
|
||||
imageMediaId,
|
||||
url,
|
||||
}: PromptBoxVideoProps) => {
|
||||
@@ -183,7 +186,7 @@ export const PromptBoxVideo = ({
|
||||
const handleEnqueue = async () => {
|
||||
setIsEnqueueing(true);
|
||||
|
||||
console.log("Handle Enqueue");
|
||||
console.log("PromptBoxVideo - Prompting with model", modelInfo);
|
||||
|
||||
setTimeout(() => {
|
||||
// TODO(bt,2025-05-08): This is a hack so we don't accidentally wind up with a permanently disabled prompt box if
|
||||
@@ -192,8 +195,10 @@ export const PromptBoxVideo = ({
|
||||
setIsEnqueueing(false);
|
||||
}, 10000);
|
||||
|
||||
const generateResponse = await FalKlingImageToVideo({
|
||||
const generateResponse = await EnqueueImageToVideo({
|
||||
model: modelInfo,
|
||||
image_media_token: referenceImages[0].mediaToken,
|
||||
//prompt: prompt,
|
||||
});
|
||||
|
||||
console.log("generateResponse", generateResponse);
|
||||
|
||||
+4
-1
@@ -5,6 +5,7 @@ import { Button } from "@storyteller/ui-button";
|
||||
|
||||
import { UsersApi } from "@storyteller/api";
|
||||
import { UserInfo } from "@storyteller/api";
|
||||
import { useLoginModalStore } from "@storyteller/ui-login-modal";
|
||||
|
||||
const usersApi = new UsersApi();
|
||||
|
||||
@@ -15,6 +16,7 @@ export interface ArtcraftAccountBlockProps {
|
||||
export const ArtcraftAccountBlock = ({
|
||||
globalAccountLogoutCallback,
|
||||
}: ArtcraftAccountBlockProps) => {
|
||||
const { triggerRecheck } = useLoginModalStore();
|
||||
const [artcraftSession, setArtcraftSession] = useState<UserInfo | undefined>(
|
||||
undefined
|
||||
);
|
||||
@@ -50,8 +52,9 @@ export const ArtcraftAccountBlock = ({
|
||||
setIsLoggedIn(false);
|
||||
setIsCheckingArtcraftSession(false);
|
||||
globalAccountLogoutCallback(); // TODO: This resets the old global application state
|
||||
triggerRecheck(); // Trigger modal to recheck session and potentially open
|
||||
} else {
|
||||
window.location.href = "/login"; // TODO(bt,2025-05-08): Once we have in-page routing, get rid of this.
|
||||
triggerRecheck(); // Open login modal instead of redirecting
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
DragEndEvent,
|
||||
} from "@dnd-kit/core";
|
||||
import {
|
||||
arrayMove,
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
verticalListSortingStrategy,
|
||||
useSortable,
|
||||
} from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faGripVertical,
|
||||
faSpinnerThird,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
interface RouterItem {
|
||||
id: string;
|
||||
name: string;
|
||||
emoji: string;
|
||||
}
|
||||
|
||||
interface SortableItemProps {
|
||||
id: string;
|
||||
name: string;
|
||||
emoji: string;
|
||||
isUpdating?: boolean;
|
||||
}
|
||||
|
||||
const SortableItem = ({
|
||||
id,
|
||||
name,
|
||||
emoji,
|
||||
isUpdating = false,
|
||||
}: SortableItemProps) => {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id });
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
className={`
|
||||
flex items-center justify-between rounded-lg bg-white/5 p-3 transition-colors duration-200
|
||||
${isUpdating ? "opacity-60" : "cursor-move hover:bg-white/10"}
|
||||
${isDragging ? "opacity-50 shadow-lg" : ""}
|
||||
`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-lg">{emoji}</span>
|
||||
<span className="font-medium">{name}</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<FontAwesomeIcon
|
||||
icon={faGripVertical}
|
||||
className="text-white/40 text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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 [isUpdating, setIsUpdating] = useState(false);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
})
|
||||
);
|
||||
|
||||
const updateRouterPriorityOnBackend = async (newOrder: RouterItem[]) => {
|
||||
try {
|
||||
setIsUpdating(true);
|
||||
console.log("Updating router priority on backend:", newOrder);
|
||||
|
||||
// Simulate API call delay
|
||||
await new Promise((resolve) => setTimeout(resolve, 500)); //remove this - BFlat
|
||||
|
||||
//api call here
|
||||
|
||||
console.log("Router priority updated successfully");
|
||||
} catch (error) {
|
||||
console.error("Failed to update router priority:", error);
|
||||
} finally {
|
||||
setIsUpdating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
|
||||
if (active.id !== over?.id) {
|
||||
setItems((prevItems) => {
|
||||
const oldIndex = prevItems.findIndex((item) => item.id === active.id);
|
||||
const newIndex = prevItems.findIndex((item) => item.id === over?.id);
|
||||
|
||||
const newOrder = arrayMove(prevItems, oldIndex, newIndex);
|
||||
|
||||
// Send update to backend
|
||||
updateRouterPriorityOnBackend(newOrder);
|
||||
|
||||
return newOrder;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext items={items} strategy={verticalListSortingStrategy}>
|
||||
<div className="space-y-2">
|
||||
{items.map((item) => (
|
||||
<SortableItem
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
name={item.name}
|
||||
emoji={item.emoji}
|
||||
isUpdating={isUpdating}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
|
||||
{isUpdating && (
|
||||
<div className="text-xs rounded-full animate-pulse mt-4 flex items-center gap-2">
|
||||
<FontAwesomeIcon icon={faSpinnerThird} className="animate-spin" />
|
||||
Updating...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -6,12 +6,14 @@ import {
|
||||
faCog,
|
||||
faVolumeHigh,
|
||||
faCircleInfo,
|
||||
faRoute,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
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";
|
||||
|
||||
interface SettingsModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -20,8 +22,12 @@ interface SettingsModalProps {
|
||||
initialSection?: SettingsSection;
|
||||
}
|
||||
|
||||
//type SettingsSection = "misc" | "audio" | "accounts" | "video" | "image";
|
||||
type SettingsSection = "general" | "accounts" | "alerts" | "about";
|
||||
type SettingsSection =
|
||||
| "general"
|
||||
| "accounts"
|
||||
| "alerts"
|
||||
| "about"
|
||||
| "router_priority";
|
||||
|
||||
export const SettingsModal = ({
|
||||
isOpen,
|
||||
@@ -37,6 +43,11 @@ export const SettingsModal = ({
|
||||
{ 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",
|
||||
icon: faRoute,
|
||||
},
|
||||
//{ id: "video" as const, label: "Video", icon: faVideo },
|
||||
//{ id: "image" as const, label: "Image", icon: faImage },
|
||||
];
|
||||
@@ -55,6 +66,8 @@ export const SettingsModal = ({
|
||||
);
|
||||
case "about":
|
||||
return <AboutSettingsPane />;
|
||||
case "router_priority":
|
||||
return <RouterPrioritySettingsPane />;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../.eslintrc.json"
|
||||
],
|
||||
"ignorePatterns": [
|
||||
"!**/*"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"*.ts",
|
||||
"*.tsx",
|
||||
"*.js",
|
||||
"*.jsx"
|
||||
],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.ts",
|
||||
"*.tsx"
|
||||
],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.js",
|
||||
"*.jsx"
|
||||
],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.json"
|
||||
],
|
||||
"parser": "jsonc-eslint-parser",
|
||||
"rules": {
|
||||
"@nx/dependency-checks": [
|
||||
"error",
|
||||
{
|
||||
"ignoredFiles": [
|
||||
"{projectRoot}/eslint.config.{js,cjs,mjs}",
|
||||
"{projectRoot}/vite.config.{js,ts,mjs,mts}"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
# model-list
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
|
||||
## Building
|
||||
|
||||
Run `nx build model-list` to build the library.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `nx test model-list` to execute the unit tests via [Vitest](https://vitest.dev/).
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "@storyteller/model-list",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
"./package.json": "./package.json",
|
||||
".": {
|
||||
"development": "./src/index.ts",
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
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';
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
// 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> = {
|
||||
"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,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
|
||||
export enum ModelCreator {
|
||||
BlackForestLabs = "BlackForestLabs",
|
||||
Bytedance = "Bytedance",
|
||||
Google = "Google",
|
||||
Hailuo = "Hailuo",
|
||||
Kling = "Kling",
|
||||
Midjourney = "Midjourney",
|
||||
OpenAi = "OpenAi",
|
||||
Runway = "Runway",
|
||||
Stability = "Stability",
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ModelCreator } from "./ModelCreator.js";
|
||||
|
||||
export interface ModelInfo {
|
||||
// Human-readible name
|
||||
// Other components may want to rename the model, eg. to fit in smaller width elements
|
||||
name: string,
|
||||
|
||||
// The identifier that Tauri uses (it may send different signals downstream)
|
||||
tauri_id: string,
|
||||
|
||||
// Creator of the model type
|
||||
creator: ModelCreator,
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
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,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { modelList } from './model-list.js';
|
||||
|
||||
describe('modelList', () => {
|
||||
it('should work', () => {
|
||||
expect(modelList()).toEqual('model-list');
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
export function modelList(): string {
|
||||
return 'model-list';
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo",
|
||||
"emitDeclarationOnly": true,
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"types": [
|
||||
"node",
|
||||
"vite/client"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"references": [],
|
||||
"exclude": [
|
||||
"vite.config.ts",
|
||||
"vite.config.mts",
|
||||
"vitest.config.ts",
|
||||
"vitest.config.mts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.test.tsx",
|
||||
"src/**/*.spec.tsx",
|
||||
"src/**/*.test.js",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.test.jsx",
|
||||
"src/**/*.spec.jsx"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/vitest",
|
||||
"types": [
|
||||
"vitest/globals",
|
||||
"vitest/importMeta",
|
||||
"vite/client",
|
||||
"node",
|
||||
"vitest"
|
||||
],
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts",
|
||||
"vite.config.mts",
|
||||
"vitest.config.ts",
|
||||
"vitest.config.mts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.test.tsx",
|
||||
"src/**/*.spec.tsx",
|
||||
"src/**/*.test.js",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.test.jsx",
|
||||
"src/**/*.spec.jsx",
|
||||
"src/**/*.d.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/// <reference types='vitest' />
|
||||
import { defineConfig } from 'vite';
|
||||
import dts from 'vite-plugin-dts';
|
||||
import * as path from 'path';
|
||||
|
||||
export default defineConfig(() => ({
|
||||
root: __dirname,
|
||||
cacheDir: '../../node_modules/.vite/libs/model-list',
|
||||
plugins: [dts({ entryRoot: 'src', tsconfigPath: path.join(__dirname, 'tsconfig.lib.json') })],
|
||||
// Uncomment this if you are using workers.
|
||||
// worker: {
|
||||
// plugins: [ nxViteTsPaths() ],
|
||||
// },
|
||||
// Configuration for building your library.
|
||||
// See: https://vitejs.dev/guide/build.html#library-mode
|
||||
build: {
|
||||
outDir: './dist',
|
||||
emptyOutDir: true,
|
||||
reportCompressedSize: true,
|
||||
commonjsOptions: {
|
||||
transformMixedEsModules: true,
|
||||
},
|
||||
lib: {
|
||||
// Could also be a dictionary or array of multiple entry points.
|
||||
entry: 'src/index.ts',
|
||||
name: '@frontend/model-list',
|
||||
fileName: 'index',
|
||||
// Change this to the formats you want to support.
|
||||
// Don't forget to update your package.json as well.
|
||||
formats: ['es' as const]
|
||||
},
|
||||
rollupOptions: {
|
||||
// External packages that should not be bundled into your library.
|
||||
external: []
|
||||
},
|
||||
},
|
||||
test: {
|
||||
watch: false,
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
||||
reporters: ['default'],
|
||||
coverage: {
|
||||
reportsDirectory: './test-output/vitest/coverage',
|
||||
provider: 'v8' as const,
|
||||
}
|
||||
},
|
||||
}));
|
||||
@@ -1,18 +1,19 @@
|
||||
export * from "./lib/common/CommandStatus.js";
|
||||
export * from "./lib/enqueue/EnqueueTextToImage.js";
|
||||
export * from "./lib/enqueue/EnqueueImageTo3dObject.js";
|
||||
export * from "./lib/enqueue/EnqueueTextToImage.js";
|
||||
export * from "./lib/enqueue/EnqueueImageToVideo.js";
|
||||
export * from "./lib/fal/FalBackgroundRemoval.js";
|
||||
export * from "./lib/fal/FalHunyuanImageTo3d.js";
|
||||
export * from "./lib/fal/FalKlingImageToVideo.js";
|
||||
export * from "./lib/fal/GetFalApiKey.js";
|
||||
export * from "./lib/fal/SetFalApiKey.js";
|
||||
export * from "./lib/openai/SetOpenAIApiKey.js";
|
||||
export * from "./lib/openai/GetOpenAIApiKey.js";
|
||||
export * from "./lib/openai/SetOpenAIApiKey.js";
|
||||
export * from "./lib/settings/GetAppPreferences.js";
|
||||
export * from "./lib/settings/UpdateAppPreference.js";
|
||||
export * from "./lib/sora/CheckSoraSession.js";
|
||||
export * from "./lib/sora/LogoutSoraSession.js";
|
||||
export * from "./lib/sora/SoraImageRemix.js";
|
||||
export * from "./lib/sora/useSoraLoginListener.js";
|
||||
export * from "./lib/system/GetAppInfo.js";
|
||||
export * from "./lib/sora/waitForSoraLogin.js";
|
||||
export * from "./lib/system/GetAppInfo.js";
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { CommandResult } from "../common/CommandStatus";
|
||||
import { ModelInfo } from "@storyteller/model-list";
|
||||
|
||||
export enum EnqueueImageToVideoErrorType {
|
||||
/// Caller didn't specify a model
|
||||
ModelNotSpecified = "model_not_specified",
|
||||
/// Generic server error
|
||||
ServerError = "server_error",
|
||||
/// No Fal API key available
|
||||
NeedsFalApiKey = "needs_fal_api_key",
|
||||
/// Fal had an API error
|
||||
FalError = "fal_error",
|
||||
}
|
||||
|
||||
export interface EnqueueImageToVideoRequest {
|
||||
image_media_token?: string;
|
||||
model?: ModelInfo;
|
||||
}
|
||||
|
||||
export interface EnqueueImageToVideoError extends CommandResult {
|
||||
error_type: EnqueueImageToVideoErrorType;
|
||||
error_message?: string;
|
||||
}
|
||||
|
||||
export interface EnqueueImageToVideoPayload {
|
||||
media_token: string;
|
||||
cdn_url: string;
|
||||
base64_bytes: string;
|
||||
}
|
||||
|
||||
export interface EnqueueImageToVideoSuccess extends CommandResult {
|
||||
payload: EnqueueImageToVideoPayload;
|
||||
}
|
||||
|
||||
export type EnqueueImageToVideoResult = EnqueueImageToVideoSuccess | EnqueueImageToVideoError;
|
||||
|
||||
export const EnqueueImageToVideo = async (request: EnqueueImageToVideoRequest) : Promise<EnqueueImageToVideoResult> => {
|
||||
const modelName = request.model?.tauri_id;
|
||||
|
||||
const result = await invoke("enqueue_image_to_video_command", {
|
||||
request: {
|
||||
image_media_token: request.image_media_token,
|
||||
model: modelName,
|
||||
}
|
||||
});
|
||||
|
||||
return (result as EnqueueImageToVideoResult);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { CommandResult } from "../common/CommandStatus";
|
||||
import { ModelInfo } from "@storyteller/model-list";
|
||||
|
||||
export enum EnqueueTextToImageErrorType {
|
||||
/// Caller didn't specify a model
|
||||
@@ -14,7 +15,7 @@ export enum EnqueueTextToImageErrorType {
|
||||
|
||||
export interface EnqueueTextToImageRequest {
|
||||
prompt?: string;
|
||||
model?: EnqueueTextToImageModel;
|
||||
model?: ModelInfo | EnqueueTextToImageModel;
|
||||
}
|
||||
|
||||
export enum EnqueueTextToImageModel {
|
||||
@@ -41,11 +42,22 @@ export interface EnqueueTextToImageSuccess extends CommandResult {
|
||||
export type EnqueueTextToImageResult = EnqueueTextToImageSuccess | EnqueueTextToImageError;
|
||||
|
||||
export const EnqueueTextToImage = async (request: EnqueueTextToImageRequest) : Promise<EnqueueTextToImageResult> => {
|
||||
let result = await invoke("enqueue_text_to_image_command", {
|
||||
let modelName = undefined;
|
||||
|
||||
if (!!request.model) {
|
||||
if (typeof request.model === "string") {
|
||||
modelName = request.model;
|
||||
} else {
|
||||
modelName = request.model.tauri_id;
|
||||
}
|
||||
}
|
||||
|
||||
const result = await invoke("enqueue_text_to_image_command", {
|
||||
request: {
|
||||
prompt: request.prompt,
|
||||
model: request.model,
|
||||
model: modelName,
|
||||
}
|
||||
});
|
||||
|
||||
return (result as EnqueueTextToImageResult);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
},
|
||||
{
|
||||
"path": "../model-list"
|
||||
}
|
||||
],
|
||||
"extends": "../../tsconfig.base.json"
|
||||
|
||||
@@ -44,5 +44,10 @@
|
||||
"src/**/*.jsx",
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../model-list/tsconfig.lib.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Generated
+60
@@ -13,6 +13,9 @@
|
||||
"libs/**/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@headlessui/react": "^2.2.2",
|
||||
"@preact/signals-react": "^3.0.1",
|
||||
"@remix-run/serve": "^2.15.0",
|
||||
@@ -1522,6 +1525,10 @@
|
||||
"name": "@frontend/login",
|
||||
"version": "0.0.1"
|
||||
},
|
||||
"libs/model-list": {
|
||||
"name": "@storyteller/model-list",
|
||||
"version": "0.0.1"
|
||||
},
|
||||
"libs/soundboard": {
|
||||
"name": "@storyteller/soundboard",
|
||||
"version": "0.0.1",
|
||||
@@ -3428,6 +3435,55 @@
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/accessibility": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
|
||||
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/core": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
|
||||
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
|
||||
"dependencies": {
|
||||
"@dnd-kit/accessibility": "^3.1.1",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/sortable": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
|
||||
"integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
|
||||
"dependencies": {
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dnd-kit/core": "^6.3.0",
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/utilities": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
|
||||
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.0.tgz",
|
||||
@@ -9649,6 +9705,10 @@
|
||||
"resolved": "libs/common",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@storyteller/model-list": {
|
||||
"resolved": "libs/model-list",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@storyteller/soundboard": {
|
||||
"resolved": "libs/soundboard",
|
||||
"link": true
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
"scripts": {},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@headlessui/react": "^2.2.2",
|
||||
"@preact/signals-react": "^3.0.1",
|
||||
"@remix-run/serve": "^2.15.0",
|
||||
|
||||
@@ -143,6 +143,9 @@
|
||||
},
|
||||
{
|
||||
"path": "./libs/components/pricing-modal"
|
||||
},
|
||||
{
|
||||
"path": "./libs/model-list"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user