diff --git a/.sqlx/query-c58f414b5e4d0f0420d1894e2f40311e5613919f49f3ac91f81f5fc2bef1bea8.json b/.sqlx/query-b4470e607c005421dd8504b9f1d695080a581936ac6ce00b6f437b2764e741be.json similarity index 50% rename from .sqlx/query-c58f414b5e4d0f0420d1894e2f40311e5613919f49f3ac91f81f5fc2bef1bea8.json rename to .sqlx/query-b4470e607c005421dd8504b9f1d695080a581936ac6ce00b6f437b2764e741be.json index 7d61829eb8..72e13fd7a8 100644 --- a/.sqlx/query-c58f414b5e4d0f0420d1894e2f40311e5613919f49f3ac91f81f5fc2bef1bea8.json +++ b/.sqlx/query-b4470e607c005421dd8504b9f1d695080a581936ac6ce00b6f437b2764e741be.json @@ -1,12 +1,12 @@ { "db_name": "MySQL", - "query": "\n INSERT INTO media_files\n SET\n token = ?,\n\n media_class = ?,\n media_type = ?,\n\n is_user_upload = ?,\n is_intermediate_system_file = ?,\n\n origin_category = ?, \n origin_product_category = ?, \n maybe_origin_model_type = ?, \n maybe_origin_model_token = ?, \n maybe_origin_filename = ?,\n\n is_batch_generated = ?,\n maybe_batch_token = ?,\n\n maybe_mime_type = ?,\n file_size_bytes = ?,\n maybe_duration_millis = ?,\n maybe_audio_encoding = ?, \n maybe_video_encoding = ?, \n maybe_frame_width = ?, \n maybe_frame_height = ?,\n maybe_prompt_token = ?,\n checksum_sha2 = ?,\n\n maybe_title = ?,\n maybe_text_transcript = ?,\n\n maybe_scene_source_media_file_token = ?,\n\n public_bucket_directory_hash = ?, \n maybe_public_bucket_prefix = ?, \n maybe_public_bucket_extension = ?, \n\n maybe_creator_user_token = ?, \n maybe_creator_anonymous_visitor_token = ?, \n\n creator_ip_address = ?, \n creator_set_visibility = ?, \n\n maybe_creator_file_synthetic_id = ?, \n maybe_creator_category_synthetic_id = ?,\n\n extra_file_modification_info = ?,\n\n maybe_mod_user_token = ?, \n is_generated_on_prem = ?, \n generated_by_worker = ?, \n generated_by_cluster = ?\n ", + "query": "\n INSERT INTO media_files\n SET\n token = ?,\n\n media_class = ?,\n media_type = ?,\n\n is_user_upload = ?,\n is_intermediate_system_file = ?,\n\n origin_category = ?, \n origin_product_category = ?, \n maybe_origin_model_type = ?, \n maybe_origin_model_token = ?, \n maybe_origin_filename = ?,\n\n is_batch_generated = ?,\n maybe_batch_token = ?,\n\n maybe_mime_type = ?,\n file_size_bytes = ?,\n maybe_duration_millis = ?,\n maybe_audio_encoding = ?, \n maybe_video_encoding = ?, \n maybe_frame_width = ?, \n maybe_frame_height = ?,\n maybe_prompt_token = ?,\n checksum_sha2 = ?,\n \n maybe_engine_category = ?,\n\n maybe_title = ?,\n maybe_text_transcript = ?,\n\n maybe_scene_source_media_file_token = ?,\n\n public_bucket_directory_hash = ?, \n maybe_public_bucket_prefix = ?, \n maybe_public_bucket_extension = ?, \n\n maybe_creator_user_token = ?, \n maybe_creator_anonymous_visitor_token = ?, \n\n creator_ip_address = ?, \n creator_set_visibility = ?, \n\n maybe_creator_file_synthetic_id = ?, \n maybe_creator_category_synthetic_id = ?,\n\n extra_file_modification_info = ?,\n\n maybe_mod_user_token = ?, \n is_generated_on_prem = ?, \n generated_by_worker = ?, \n generated_by_cluster = ?\n ", "describe": { "columns": [], "parameters": { - "Right": 38 + "Right": 39 }, "nullable": [] }, - "hash": "c58f414b5e4d0f0420d1894e2f40311e5613919f49f3ac91f81f5fc2bef1bea8" + "hash": "b4470e607c005421dd8504b9f1d695080a581936ac6ce00b6f437b2764e741be" } diff --git a/Cargo.lock b/Cargo.lock index 272d334a0b..c5ffc174b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -648,6 +648,7 @@ name = "artcraft" version = "0.1.0" dependencies = [ "anyhow", + "artcraft_api_defs", "base64 0.22.1", "build_metadata", "bytes 1.10.1", @@ -659,6 +660,7 @@ dependencies = [ "fal_client", "filesys", "futures 0.3.31", + "idempotency", "image", "indexmap 2.9.0", "log", @@ -9994,6 +9996,7 @@ name = "storyteller_client" version = "0.0.1" dependencies = [ "anyhow", + "artcraft_api_defs", "backoff", "chrono", "cookie 0.18.1", diff --git a/crates/desktop/artcraft/Cargo.toml b/crates/desktop/artcraft/Cargo.toml index a65a686202..494d9ec0fe 100644 --- a/crates/desktop/artcraft/Cargo.toml +++ b/crates/desktop/artcraft/Cargo.toml @@ -23,10 +23,12 @@ tauri-build = { version = "2.1.1", features = [] } workspace-build-acceleration = { workspace = true } # Internal +artcraft_api_defs = { workspace = true } build_metadata = { workspace = true } errors = { workspace = true } fal_client = { workspace = true } filesys = { workspace = true } +idempotency = { workspace = true } memory_store = { workspace = true } mimetypes = { workspace = true } openai_sora_client = { workspace = true } diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/mod.rs b/crates/desktop/artcraft/src/core/commands/enqueue/mod.rs index b1b6b4f003..a91780e378 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/mod.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/mod.rs @@ -1 +1,2 @@ -pub mod image; \ No newline at end of file +pub mod image; +pub mod object; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/object/enqueue_image_to_3d_object_command.rs b/crates/desktop/artcraft/src/core/commands/enqueue/object/enqueue_image_to_3d_object_command.rs new file mode 100644 index 0000000000..eab5e57614 --- /dev/null +++ b/crates/desktop/artcraft/src/core/commands/enqueue/object/enqueue_image_to_3d_object_command.rs @@ -0,0 +1,179 @@ +use crate::core::commands::enqueue::image::handle_fal::handle_fal; +use crate::core::commands::enqueue::image::handle_sora::handle_sora; +use crate::core::commands::enqueue::object::handle_object_fal::handle_object_fal; +use crate::core::commands::enqueue::object::internal_object_error::InternalObjectError; +use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus}; +use crate::core::commands::response::shorthand::Response; +use crate::core::commands::response::success_response_wrapper::SerializeMarker; +use crate::core::events::sendable_event_trait::SendableEvent; +use crate::core::state::data_dir::app_data_root::AppDataRoot; +use crate::core::state::data_dir::trait_data_subdir::DataSubdir; +use crate::core::utils::download_media_file_to_temp_dir::download_media_file_to_temp_dir; +use crate::core::utils::get_url_file_extension::get_url_file_extension; +use crate::core::utils::save_base64_image_to_temp_dir::save_base64_image_to_temp_dir; +use crate::core::utils::simple_http_download::simple_http_download; +use crate::core::utils::simple_http_download_to_tempfile::simple_http_download_to_tempfile; +use crate::services::fal::state::fal_credential_manager::FalCredentialManager; +use crate::services::fal::state::fal_task_queue::FalTaskQueue; +use crate::services::sora::state::read_sora_credentials_from_disk::read_sora_credentials_from_disk; +use crate::services::sora::state::sora_credential_holder::SoraCredentialHolder; +use crate::services::sora::state::sora_credential_manager::SoraCredentialManager; +use crate::services::sora::state::sora_task_queue::SoraTaskQueue; +use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager; +use anyhow::anyhow; +use base64::prelude::BASE64_STANDARD; +use base64::{DecodeError, Engine}; +use errors::{AnyhowError, AnyhowResult}; +use fal_client::error::fal_error_plus::FalErrorPlus; +use fal_client::requests::queue::image_gen::enqueue_flux_pro_ultra_text_to_image::{enqueue_flux_pro_ultra_text_to_image, FluxProUltraTextToImageArgs}; +use fal_client::requests::queue::image_gen::enqueue_recraft3_text_to_image::{enqueue_recraft3_text_to_image, Recraft3TextToImageArgs}; +use filesys::file_read_bytes::file_read_bytes; +use image::codecs::png::{CompressionType, FilterType, PngEncoder}; +use image::{DynamicImage, EncodableLayout, ImageReader}; +use log::{debug, error, info, warn}; +use mimetypes::mimetype_info::mimetype_info::MimetypeInfo; +use openai_sora_client::credentials::SoraCredentials; +use openai_sora_client::creds::credential_migration::CredentialMigrationRef; +use openai_sora_client::recipes::image_remix_with_session_auto_renew::{image_remix_with_session_auto_renew, ImageRemixAutoRenewRequest}; +use openai_sora_client::recipes::image_upload_from_file_with_session_auto_renew::{image_upload_from_file_with_session_auto_renew, ImageUploadFromFileAutoRenewRequest}; +use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session; +use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages}; +use openai_sora_client::requests::image_gen::sora_image_gen_remix::{sora_image_gen_remix, SoraImageGenRemixRequest}; +use openai_sora_client::requests::upload::upload_media_from_bytes::sora_media_upload_from_bytes; +use openai_sora_client::requests::upload::upload_media_from_file::sora_media_upload_from_file; +use openai_sora_client::sora_error::SoraError; +use reqwest::Url; +use serde_derive::{Deserialize, Serialize}; +use std::fs::read_to_string; +use std::io::{Cursor, Read, Write}; +use std::ops::Add; +use std::path::PathBuf; +use std::time::Duration; +use storyteller_client::error::api_error::ApiError; +use storyteller_client::error::api_error::ApiError::InternalServerError; +use storyteller_client::media_files::get_media_file::{get_media_file, GetMediaFileSuccessResponse}; +use storyteller_client::media_files::upload_image_media_file_from_file::upload_image_media_file_from_file; +use storyteller_client::utils::api_host::ApiHost; +use tauri::{AppHandle, Emitter, Manager, State}; +use tempfile::NamedTempFile; +use tokens::tokens::media_files::MediaFileToken; +use crate::core::commands::enqueue::object::handle_object_artcraft::handle_object_artcraft; + +#[derive(Deserialize)] +pub struct EnqueueImageTo3dObjectRequest { + /// Image media file; the image to remove the background from. + /// TODO: In the future we may support base64 images, URLs, or file paths here. + pub image_media_token: Option, + + /// The model to use. + pub model: Option, +} + +#[derive(Deserialize, Debug, Copy, Clone)] +#[serde(rename_all = "snake_case")] +pub enum EnqueueImageTo3dObjectModel { + #[serde(rename = "hunyuan_3d_2")] + Hunyuan3d2, +} + +#[derive(Serialize)] +pub struct EnqueueImageTo3dObjectSuccessResponse { +} + +impl SerializeMarker for EnqueueImageTo3dObjectSuccessResponse {} + +#[derive(Serialize, Debug)] +#[serde(rename_all = "snake_case")] +pub enum EnqueueImageTo3dObjectErrorType { + /// Caller didn't specify a model + ModelNotSpecified, + /// Generic server error + ServerError, + /// No Fal API key available + NeedsFalApiKey, + /// Fal had an API error + FalError, + /// Needs to be logged into Artcraft + NeedsStorytellerCredentials, +} + +#[tauri::command] +pub async fn enqueue_image_to_3d_object_command( + app: AppHandle, + request: EnqueueImageTo3dObjectRequest, + app_data_root: State<'_, AppDataRoot>, + fal_creds_manager: State<'_, FalCredentialManager>, + fal_task_queue: State<'_, FalTaskQueue>, + storyteller_creds_manager: State<'_, StorytellerCredentialManager>, + sora_task_queue: State<'_, SoraTaskQueue>, +) -> Response { + + info!("enqueue_image_to_3d_object_command called"); + + let result = handle_request( + &app, + request, + &app_data_root, + &fal_creds_manager, + &storyteller_creds_manager, + &fal_task_queue, + ).await; + + match result { + Err(err) => { + error!("error: {:?}", err); + + let mut status = CommandErrorStatus::ServerError; + let mut error_type = EnqueueImageTo3dObjectErrorType::ServerError; + let mut error_message = "A server error occurred. Please try again. If it continues, please tell our staff about the problem."; + + match err { + InternalObjectError::NoModelSpecified => { + status = CommandErrorStatus::BadRequest; + error_type = EnqueueImageTo3dObjectErrorType::ModelNotSpecified; + error_message = "No model specified for image generation"; + } + InternalObjectError::NeedsFalApiKey => { + status = CommandErrorStatus::Unauthorized; + error_type = EnqueueImageTo3dObjectErrorType::NeedsFalApiKey; + error_message = "You need to set a FAL api key"; + }, + InternalObjectError::NeedsStorytellerCredentials => { + status = CommandErrorStatus::Unauthorized; + error_type = EnqueueImageTo3dObjectErrorType::NeedsStorytellerCredentials; + error_message = "You need to be logged into Artcraft."; + } + _ => {}, // Fall-through + } + + Err(CommandErrorResponseWrapper { + status, + error_message: Some(error_message.to_string()), + error_type: Some(error_type), + error_details: None, + }) + } + Ok(()) => { + Ok(EnqueueImageTo3dObjectSuccessResponse {}.into()) + } + } +} + + +pub async fn handle_request( + app: &AppHandle, + request: EnqueueImageTo3dObjectRequest, + app_data_root: &AppDataRoot, + fal_creds_manager: &FalCredentialManager, + storyteller_creds_manager: &StorytellerCredentialManager, + fal_task_queue: &FalTaskQueue, +) -> Result<(), InternalObjectError> { + + if fal_creds_manager.has_apparent_api_token()? { + handle_object_fal(&app, app_data_root, request, fal_creds_manager, fal_task_queue).await?; + } else { + handle_object_artcraft(request, &app, app_data_root, storyteller_creds_manager).await?; + } + + Ok(()) +} diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/object/handle_object_artcraft.rs b/crates/desktop/artcraft/src/core/commands/enqueue/object/handle_object_artcraft.rs new file mode 100644 index 0000000000..0563d9c9be --- /dev/null +++ b/crates/desktop/artcraft/src/core/commands/enqueue/object/handle_object_artcraft.rs @@ -0,0 +1,129 @@ +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; +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::state::data_dir::app_data_root::AppDataRoot; +use crate::core::utils::download_media_file_to_temp_dir::download_media_file_to_temp_dir; +use crate::core::utils::save_base64_image_to_temp_dir::save_base64_image_to_temp_dir; +use crate::services::fal::state::fal_credential_manager::FalCredentialManager; +use crate::services::fal::state::fal_task_queue::FalTaskQueue; +use crate::services::sora::state::sora_credential_manager::SoraCredentialManager; +use crate::services::sora::state::sora_task_queue::SoraTaskQueue; +use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager; +use anyhow::anyhow; +use artcraft_api_defs::generate::object::generate_hunyuan_2_image_to_3d::GenerateHunyuan2ImageTo3dRequest; +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_ultra_text_to_image::{enqueue_flux_pro_ultra_text_to_image, FluxProUltraTextToImageArgs}; +use fal_client::requests::queue::image_gen::enqueue_recraft3_text_to_image::{enqueue_recraft3_text_to_image, Recraft3TextToImageArgs}; +use idempotency::uuid::generate_random_uuid; +use log::{error, info, warn}; +use storyteller_client::generate::object::generate_hunyuan2_image_to_3d::generate_hunyuan2_image_to_3d; +use storyteller_client::utils::api_host::ApiHost; +use tauri::{AppHandle, State}; + +pub async fn handle_object_artcraft( + request: EnqueueImageTo3dObjectRequest, + app: &AppHandle, + app_data_root: &AppDataRoot, + storyteller_creds_manager: &StorytellerCredentialManager, +) -> Result<(), InternalObjectError> { + + let creds = match storyteller_creds_manager.get_credentials()? { + Some(creds) => creds, + None => { + error!("No Artcraft credentials are set. Can't perform action."); + let event = + GenerationEnqueueFailureEvent::no_artcraft_credentials(GenerationAction::ImageTo3d); + + if let Err(err) = event.send(&app) { + error!("Failed to emit event: {:?}", err); // Fail open. + } + + return Err(InternalObjectError::NeedsStorytellerCredentials); + }, + }; + + //let api_key = fal_creds_manager.get_key_required() + // .map_err(|err| { + // error!("EnqueueTextToImage FAL api key required: {:?}", err); + // InternalObjectError::NeedsFalApiKey + // })?; + + + info!("Calling Artcraft image to 3d ..."); + + + let uuid_idempotency_token = generate_random_uuid(); + + let job_token = match request.model { + None => { + return Err(InternalObjectError::NoModelSpecified); + } + Some(EnqueueImageTo3dObjectModel::Hunyuan3d2) => { + info!("enqueue Hunyuan 3D 2.0"); + let request = GenerateHunyuan2ImageTo3dRequest { + uuid_idempotency_token, + media_file_token: request.image_media_token, + }; + let result = generate_hunyuan2_image_to_3d( + &ApiHost::Storyteller, + Some(&creds), + request, + ).await; + match result { + Ok(enqueued) => { + info!("Successfully enqueued Artcraft Hunyuan 3D 2.0"); + enqueued.inference_job_token + } + Err(err) => { + error!("Failed to use Artcraft Hunyuan 3D 2.0: {:?}", err); + return Err(InternalObjectError::StorytellerError(err)); + } + } + } + }; + + info!("Successfully enqueued 3D generation"); + + let event = GenerationEnqueueSuccessEvent { + action: GenerationAction::ImageTo3d, + service: GenerationServiceProvider::Artcraft, + model: None, + }; + + 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); + // return Err(InternalObjectError::AnyhowError(anyhow!("Failed to enqueue task: {:?}", err))); + //} + +// match result { +// Ok(enqueued) => { +// } +// Err(err) => { +// error!("Failed to enqueue image to 3d: {:?}", err); +// +// let event = GenerationEnqueueFailureEvent { +// action: GenerationAction::ImageTo3d, +// service: GenerationServiceProvider::Fal, +// model: None, +// reason: None, +// }; +// +// if let Err(err) = event.send(app) { +// error!("Failed to emit event: {:?}", err); // Fail open. +// } +// +// return Err(InternalObjectError::FalError(err)); +// } +// } + + Ok(()) +} diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/object/handle_object_fal.rs b/crates/desktop/artcraft/src/core/commands/enqueue/object/handle_object_fal.rs new file mode 100644 index 0000000000..eb26daa750 --- /dev/null +++ b/crates/desktop/artcraft/src/core/commands/enqueue/object/handle_object_fal.rs @@ -0,0 +1,117 @@ +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; +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::state::data_dir::app_data_root::AppDataRoot; +use crate::core::utils::download_media_file_to_temp_dir::download_media_file_to_temp_dir; +use crate::core::utils::save_base64_image_to_temp_dir::save_base64_image_to_temp_dir; +use crate::services::fal::state::fal_credential_manager::FalCredentialManager; +use crate::services::fal::state::fal_task_queue::FalTaskQueue; +use crate::services::sora::state::sora_credential_manager::SoraCredentialManager; +use crate::services::sora::state::sora_task_queue::SoraTaskQueue; +use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager; +use anyhow::anyhow; +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_ultra_text_to_image::{enqueue_flux_pro_ultra_text_to_image, FluxProUltraTextToImageArgs}; +use fal_client::requests::queue::image_gen::enqueue_recraft3_text_to_image::{enqueue_recraft3_text_to_image, Recraft3TextToImageArgs}; +use log::{error, info, warn}; +use tauri::AppHandle; + +pub async fn handle_object_fal( + app: &AppHandle, + app_data_root: &AppDataRoot, + request: EnqueueImageTo3dObjectRequest, + fal_creds_manager: &FalCredentialManager, + fal_task_queue: &FalTaskQueue, +) -> Result<(), InternalObjectError> { + + let api_key = match fal_creds_manager.get_key()? { + Some(key) => key, + None => { + error!("No FAL API key is set. Can't perform action."); + let event = + GenerationEnqueueFailureEvent::no_fal_api_key(GenerationAction::GenerateImage); + + if let Err(err) = event.send(&app) { + error!("Failed to emit event: {:?}", err); // Fail open. + } + + return Err(InternalObjectError::NeedsFalApiKey); + }, + }; + + //let api_key = fal_creds_manager.get_key_required() + // .map_err(|err| { + // error!("EnqueueTextToImage FAL api key required: {:?}", err); + // InternalObjectError::NeedsFalApiKey + // })?; + + let mut temp_download; + + if let Some(media_token) = request.image_media_token { + temp_download = download_media_file_to_temp_dir(&app_data_root, &media_token).await?; + + } else { + return Err(InternalObjectError::AnyhowError(anyhow!("No image media token provided"))); + } + + info!("Calling FAL image to 3d ..."); + + let filename = temp_download.path().to_path_buf(); + + let result = match request.model { + None => { + return Err(InternalObjectError::NoModelSpecified); + } + Some(EnqueueImageTo3dObjectModel::Hunyuan3d2) => { + info!("enqueue Hunyuan 3D 2.0"); + enqueue_hunyuan2_image_to_3d(Hunyuan2Args { + image_path: filename, + api_key: &api_key, + }).await + } + }; + + match result { + Ok(enqueued) => { + info!("Successfully enqueued text to image"); + + let event = GenerationEnqueueSuccessEvent { + action: GenerationAction::ImageTo3d, + service: GenerationServiceProvider::Fal, + model: None, + }; + + 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); + return Err(InternalObjectError::AnyhowError(anyhow!("Failed to enqueue task: {:?}", err))); + } + } + Err(err) => { + error!("Failed to enqueue image to 3d: {:?}", err); + + let event = GenerationEnqueueFailureEvent { + action: GenerationAction::ImageTo3d, + service: GenerationServiceProvider::Fal, + model: None, + reason: None, + }; + + if let Err(err) = event.send(app) { + error!("Failed to emit event: {:?}", err); // Fail open. + } + + return Err(InternalObjectError::FalError(err)); + } + } + + Ok(()) +} diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/object/internal_object_error.rs b/crates/desktop/artcraft/src/core/commands/enqueue/object/internal_object_error.rs new file mode 100644 index 0000000000..bab6d0d7cd --- /dev/null +++ b/crates/desktop/artcraft/src/core/commands/enqueue/object/internal_object_error.rs @@ -0,0 +1,49 @@ +use base64::DecodeError; +use errors::AnyhowError; +use fal_client::error::fal_error_plus::FalErrorPlus; +use storyteller_client::error::api_error::ApiError; +use storyteller_client::error::storyteller_error::StorytellerError; + +#[derive(Debug)] +pub enum InternalObjectError { + NoModelSpecified, + NeedsFalApiKey, + NeedsSoraCredentials, + NeedsStorytellerCredentials, + FalError(FalErrorPlus), + AnyhowError(AnyhowError), + StorytellerError(StorytellerError), + StorytellerApiError(ApiError), + DecodeError(DecodeError), + IoError(std::io::Error), +} + +impl From for InternalObjectError { + fn from(value: AnyhowError) -> Self { + Self::AnyhowError(value) + } +} + +impl From for InternalObjectError { + fn from(value: FalErrorPlus) -> Self { + Self::FalError(value) + } +} + +impl From for InternalObjectError { + fn from(value: ApiError) -> Self { + Self::StorytellerApiError(value) + } +} + +impl From for InternalObjectError { + fn from(value: DecodeError) -> Self { + Self::DecodeError(value) + } +} + +impl From for InternalObjectError { + fn from(value: std::io::Error) -> Self { + Self::IoError(value) + } +} diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/object/mod.rs b/crates/desktop/artcraft/src/core/commands/enqueue/object/mod.rs new file mode 100644 index 0000000000..278a282e57 --- /dev/null +++ b/crates/desktop/artcraft/src/core/commands/enqueue/object/mod.rs @@ -0,0 +1,4 @@ +pub mod enqueue_image_to_3d_object_command; +pub mod handle_object_artcraft; +pub mod handle_object_fal; +pub mod internal_object_error; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/core/events/generation_events/common.rs b/crates/desktop/artcraft/src/core/events/generation_events/common.rs index 9178aee106..19649aebb8 100644 --- a/crates/desktop/artcraft/src/core/events/generation_events/common.rs +++ b/crates/desktop/artcraft/src/core/events/generation_events/common.rs @@ -3,8 +3,9 @@ use serde::Serialize; #[derive(Debug, Copy, Clone, Serialize)] #[serde(rename_all = "snake_case")] pub enum GenerationServiceProvider { - Sora, + Artcraft, Fal, + Sora, } #[derive(Debug, Copy, Clone, Serialize)] diff --git a/crates/desktop/artcraft/src/core/events/generation_events/generation_enqueue_failure_event.rs b/crates/desktop/artcraft/src/core/events/generation_events/generation_enqueue_failure_event.rs index 0de37113f5..8930bc5ca2 100644 --- a/crates/desktop/artcraft/src/core/events/generation_events/generation_enqueue_failure_event.rs +++ b/crates/desktop/artcraft/src/core/events/generation_events/generation_enqueue_failure_event.rs @@ -19,6 +19,15 @@ impl BasicSendableEvent for GenerationEnqueueFailureEvent { } impl GenerationEnqueueFailureEvent { + pub fn no_artcraft_credentials(action: GenerationAction) -> Self { + Self { + action, + service: GenerationServiceProvider::Artcraft, + model: None, + reason: Some("Please log into Artcraft.".to_string()), + } + } + pub fn no_fal_api_key(action: GenerationAction) -> Self { Self { action, diff --git a/crates/desktop/artcraft/src/lib.rs b/crates/desktop/artcraft/src/lib.rs index 004868e3f9..2eb80820a9 100644 --- a/crates/desktop/artcraft/src/lib.rs +++ b/crates/desktop/artcraft/src/lib.rs @@ -6,6 +6,7 @@ use tauri::Manager; use crate::core::commands::app_preferences::get_app_preferences_command::get_app_preferences_command; use crate::core::commands::app_preferences::update_app_preference_command::update_app_preferences_command; use crate::core::commands::enqueue::image::enqueue_text_to_image_command::enqueue_text_to_image_command; +use crate::core::commands::enqueue::object::enqueue_image_to_3d_object_command::enqueue_image_to_3d_object_command; use crate::core::commands::flip_image::flip_image; use crate::core::commands::get_build_info_command::get_build_info_command; use crate::core::commands::platform_info_command::platform_info_command; @@ -161,6 +162,7 @@ pub fn run() { .manage(storyteller_creds_manager_3) .invoke_handler(tauri::generate_handler![ check_sora_session_command, + enqueue_image_to_3d_object_command, enqueue_text_to_image_command, fal_background_removal_command, fal_hunyuan_image_to_3d_command, diff --git a/crates/desktop/artcraft/src/services/fal/commands/fal_hunyuan_image_to_3d_command.rs b/crates/desktop/artcraft/src/services/fal/commands/fal_hunyuan_image_to_3d_command.rs index 3fd602df5c..12cfb18759 100644 --- a/crates/desktop/artcraft/src/services/fal/commands/fal_hunyuan_image_to_3d_command.rs +++ b/crates/desktop/artcraft/src/services/fal/commands/fal_hunyuan_image_to_3d_command.rs @@ -196,7 +196,6 @@ pub async fn image_to_3d( ) -> Result<(), InnerError> { let api_key = fal_creds_manager.get_key_required()?; - let creds = storyteller_creds_manager.get_credentials_required()?; let mut temp_download; diff --git a/crates/lib/api_clients/storyteller_client/Cargo.toml b/crates/lib/api_clients/storyteller_client/Cargo.toml index 03d6332dac..658e593b13 100644 --- a/crates/lib/api_clients/storyteller_client/Cargo.toml +++ b/crates/lib/api_clients/storyteller_client/Cargo.toml @@ -17,6 +17,7 @@ path = "src/lib.rs" workspace-build-acceleration = { workspace = true } # Internal +artcraft_api_defs = { workspace = true } enums = { workspace = true } enums_public = { workspace = true } errors = { workspace = true } diff --git a/crates/lib/api_clients/storyteller_client/src/error/client_error.rs b/crates/lib/api_clients/storyteller_client/src/error/client_error.rs index 1bf04115c1..784e6be546 100644 --- a/crates/lib/api_clients/storyteller_client/src/error/client_error.rs +++ b/crates/lib/api_clients/storyteller_client/src/error/client_error.rs @@ -2,6 +2,7 @@ use std::error::Error; use std::fmt::{Display, Formatter}; use std::io; use std::path::PathBuf; +use crate::error::api_error::ApiError; /// An error arising from the client, either before or after the request is sent. #[derive(Debug)] @@ -14,6 +15,12 @@ pub enum ClientError { /// An error doing file I/O (on our side) IoError(io::Error), + + /// An error setting up the client + ReqwestError(reqwest::Error), + + /// A serialization error with the request. + SerializationError(serde_json::Error), } impl Error for ClientError {} @@ -24,6 +31,8 @@ impl Display for ClientError { Self::FileTypeNotKnown(path) => write!(f, "Could not determine the filetype for the file: {:?}", path), Self::FileTypeNotHandled(path) => write!(f, "Could not handle the file type for the file: {:?}", path), Self::IoError(err) => write!(f, "IO error: {}", err), + Self::ReqwestError(err) => write!(f, "Reqwest client error: {}", err), + Self::SerializationError(err) => write!(f, "Request serialization error: {}", err), } } } @@ -33,3 +42,15 @@ impl From for ClientError { ClientError::IoError(error) } } + +impl From for ClientError { + fn from(error: serde_json::Error) -> Self { + ClientError::SerializationError(error) + } +} + +impl From for ClientError { + fn from(error: reqwest::Error) -> Self { + ClientError::ReqwestError(error) + } +} diff --git a/crates/lib/api_clients/storyteller_client/src/generate/mod.rs b/crates/lib/api_clients/storyteller_client/src/generate/mod.rs new file mode 100644 index 0000000000..305645c9d4 --- /dev/null +++ b/crates/lib/api_clients/storyteller_client/src/generate/mod.rs @@ -0,0 +1 @@ +pub mod object; \ No newline at end of file diff --git a/crates/lib/api_clients/storyteller_client/src/generate/object/generate_hunyuan2_image_to_3d.rs b/crates/lib/api_clients/storyteller_client/src/generate/object/generate_hunyuan2_image_to_3d.rs new file mode 100644 index 0000000000..789efa1ebb --- /dev/null +++ b/crates/lib/api_clients/storyteller_client/src/generate/object/generate_hunyuan2_image_to_3d.rs @@ -0,0 +1,69 @@ +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 artcraft_api_defs::generate::object::generate_hunyuan_2_image_to_3d::{GenerateHunyuan2ImageTo3dRequest, GenerateHunyuan2ImageTo3dResponse, GENERATE_HUNYUAN_2_IMAGE_TO_3D_URL_PATH}; +use log::{debug, info}; +use reqwest::Client; + +/// Upload an image media file from a file. +pub async fn generate_hunyuan2_image_to_3d( + api_host: &ApiHost, + maybe_creds: Option<&StorytellerCredentialSet>, + request: GenerateHunyuan2ImageTo3dRequest, +) -> Result { + + 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_IMAGE_TO_3D_URL_PATH) +} diff --git a/crates/lib/api_clients/storyteller_client/src/generate/object/mod.rs b/crates/lib/api_clients/storyteller_client/src/generate/object/mod.rs new file mode 100644 index 0000000000..c46e4c4b04 --- /dev/null +++ b/crates/lib/api_clients/storyteller_client/src/generate/object/mod.rs @@ -0,0 +1 @@ +pub mod generate_hunyuan2_image_to_3d; \ No newline at end of file diff --git a/crates/lib/api_clients/storyteller_client/src/lib.rs b/crates/lib/api_clients/storyteller_client/src/lib.rs index e1d6792184..228b2137de 100644 --- a/crates/lib/api_clients/storyteller_client/src/lib.rs +++ b/crates/lib/api_clients/storyteller_client/src/lib.rs @@ -2,6 +2,7 @@ pub mod credentials; pub mod error; +pub mod generate; pub mod media_files; pub mod recipes; pub mod shared_response_types; diff --git a/crates/schema/database/mysql_queries/src/queries/media_files/create/generic_insert/insert_media_file_generic.rs b/crates/schema/database/mysql_queries/src/queries/media_files/create/generic_insert/insert_media_file_generic.rs index deedbad43f..d132b59188 100644 --- a/crates/schema/database/mysql_queries/src/queries/media_files/create/generic_insert/insert_media_file_generic.rs +++ b/crates/schema/database/mysql_queries/src/queries/media_files/create/generic_insert/insert_media_file_generic.rs @@ -4,6 +4,7 @@ use sqlx::MySqlPool; use enums::by_table::generic_synthetic_ids::id_category::IdCategory; use enums::by_table::media_files::media_file_class::MediaFileClass; +use enums::by_table::media_files::media_file_engine_category::MediaFileEngineCategory; use enums::by_table::media_files::media_file_origin_category::MediaFileOriginCategory; use enums::by_table::media_files::media_file_origin_model_type::MediaFileOriginModelType; use enums::by_table::media_files::media_file_origin_product_category::MediaFileOriginProductCategory; @@ -51,6 +52,9 @@ pub struct InsertArgs<'a> { pub maybe_frame_width: Option, pub maybe_frame_height: Option, pub checksum_sha2: &'a str, + + // Media info for certain product areas + pub maybe_engine_category: Option, // TODO: Deprecate // User text information pub maybe_title: Option<&'a str>, @@ -152,6 +156,8 @@ pub async fn insert_media_file_generic( maybe_frame_height = ?, maybe_prompt_token = ?, checksum_sha2 = ?, + + maybe_engine_category = ?, maybe_title = ?, maybe_text_transcript = ?, @@ -205,6 +211,8 @@ pub async fn insert_media_file_generic( args.maybe_prompt_token.map(|t| t.as_str()), args.checksum_sha2, + args.maybe_engine_category.map(|e| e.to_str()), + args.maybe_title, args.maybe_text_transcript, diff --git a/crates/schema/database/mysql_queries/src/queries/media_files/create/generic_insert/insert_media_file_generic_from_job.rs b/crates/schema/database/mysql_queries/src/queries/media_files/create/generic_insert/insert_media_file_generic_from_job.rs index cae2cdf10b..46d9ccd561 100644 --- a/crates/schema/database/mysql_queries/src/queries/media_files/create/generic_insert/insert_media_file_generic_from_job.rs +++ b/crates/schema/database/mysql_queries/src/queries/media_files/create/generic_insert/insert_media_file_generic_from_job.rs @@ -107,6 +107,7 @@ pub async fn insert_media_file_generic_from_job( maybe_extra_media_info: args.maybe_extra_media_info, maybe_frame_height: args.maybe_frame_height, maybe_frame_width: args.maybe_frame_width, + maybe_engine_category: None, maybe_mime_type: args.maybe_mime_type, maybe_mod_user_token: args.maybe_mod_user_token, maybe_origin_filename: args.maybe_origin_filename, diff --git a/crates/schema/database/mysql_queries/src/queries/media_files/create/insert_builder/media_file_insert_builder.rs b/crates/schema/database/mysql_queries/src/queries/media_files/create/insert_builder/media_file_insert_builder.rs index 40825997d8..a156b95d67 100644 --- a/crates/schema/database/mysql_queries/src/queries/media_files/create/insert_builder/media_file_insert_builder.rs +++ b/crates/schema/database/mysql_queries/src/queries/media_files/create/insert_builder/media_file_insert_builder.rs @@ -9,6 +9,7 @@ use enums::by_table::media_files::media_file_type::MediaFileType; use enums::common::visibility::Visibility; use errors::AnyhowResult; use sqlx::MySqlPool; +use enums::by_table::media_files::media_file_engine_category::MediaFileEngineCategory; use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken; use tokens::tokens::media_files::MediaFileToken; use tokens::tokens::users::UserToken; @@ -51,6 +52,9 @@ pub struct MediaFileInsertBuilder { maybe_frame_height: Option, checksum_sha2: Option, // NB: Non-nullable field + // Media info for certain product areas + pub maybe_engine_category: Option, // TODO: Deprecate + // // User text information // maybe_title: Option<&'a str>, // maybe_text_transcript: Option<&'a str>, @@ -94,6 +98,7 @@ impl MediaFileInsertBuilder { maybe_frame_width: None, maybe_frame_height: None, checksum_sha2: None, + maybe_engine_category: None, public_bucket_directory_hash: None, } } @@ -207,6 +212,11 @@ impl MediaFileInsertBuilder { self } + pub fn maybe_engine_category(mut self, maybe_engine_category: Option) -> Self { + self.maybe_engine_category = maybe_engine_category; + self + } + // TODO: maybe_title // TODO: maybe_text_transcript // TODO: maybe_scene_source_media_file_token @@ -258,6 +268,7 @@ impl MediaFileInsertBuilder { maybe_video_encoding: None, // TODO maybe_frame_width: self.maybe_frame_width, maybe_frame_height: self.maybe_frame_height, + maybe_engine_category: self.maybe_engine_category, checksum_sha2: &checksum_sha2, maybe_title: None, // TODO maybe_text_transcript: None, // TODO diff --git a/crates/schema/database/mysql_queries/src/queries/media_files/create/insert_media_file_from_studio_scene_render.rs b/crates/schema/database/mysql_queries/src/queries/media_files/create/insert_media_file_from_studio_scene_render.rs index e7a0f764ac..846b187a34 100644 --- a/crates/schema/database/mysql_queries/src/queries/media_files/create/insert_media_file_from_studio_scene_render.rs +++ b/crates/schema/database/mysql_queries/src/queries/media_files/create/insert_media_file_from_studio_scene_render.rs @@ -111,6 +111,7 @@ pub async fn insert_media_file_from_studio_scene_render( maybe_batch_token: None, maybe_prompt_token: None, maybe_mod_user_token: None, + maybe_engine_category: None, }).await?; Ok(new_media_token) diff --git a/crates/schema/public/api/artcraft_api_defs/src/generate/object/generate_hunyuan_2_image_to_3d.rs b/crates/schema/public/api/artcraft_api_defs/src/generate/object/generate_hunyuan_2_image_to_3d.rs index 6cd96633bb..b32a4373e1 100644 --- a/crates/schema/public/api/artcraft_api_defs/src/generate/object/generate_hunyuan_2_image_to_3d.rs +++ b/crates/schema/public/api/artcraft_api_defs/src/generate/object/generate_hunyuan_2_image_to_3d.rs @@ -3,7 +3,7 @@ use tokens::tokens::generic_inference_jobs::InferenceJobToken; use tokens::tokens::media_files::MediaFileToken; use utoipa::ToSchema; -const GENERATE_HUNYUAN_2_IMAGE_TO_3D_URL_PATH: &str = "/v1/generate/object/hunyuan_2_image_to_3d"; +pub const GENERATE_HUNYUAN_2_IMAGE_TO_3D_URL_PATH: &str = "/v1/generate/object/hunyuan_2_image_to_3d"; #[derive(Serialize, Deserialize, ToSchema)] pub struct GenerateHunyuan2ImageTo3dRequest { @@ -13,9 +13,6 @@ pub struct GenerateHunyuan2ImageTo3dRequest { /// Source of the file to convert to 3D /// It must be an image. pub media_file_token: Option, - - /// Optional text prompt. - pub prompt: Option, } #[derive(Serialize, Deserialize, ToSchema)] diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/object/generate_hunyuan2_image_to_3d_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/object/generate_hunyuan2_image_to_3d_handler.rs index 425282d667..cca36b1b8a 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/object/generate_hunyuan2_image_to_3d_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/object/generate_hunyuan2_image_to_3d_handler.rs @@ -167,11 +167,6 @@ pub async fn generate_hunyuan_2_image_to_3d_handler( info!("Fal webhook URL: {}", server_state.fal.webhook_url); - let prompt = request.prompt - .as_deref() - .map(|prompt| prompt.trim()) - .unwrap_or_else(|| ""); - let args = Hunyuan2Args { image_url: media_links.cdn_url, webhook_url: &server_state.fal.webhook_url, diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/image_studio/update_gpt_image_job_status_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/image_studio/update_gpt_image_job_status_handler.rs index 1d9425d64d..4e9ffed07f 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/image_studio/update_gpt_image_job_status_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/image_studio/update_gpt_image_job_status_handler.rs @@ -32,7 +32,6 @@ use mysql_queries::queries::generic_inference::web::get_inference_job_status::{g use mysql_queries::queries::generic_inference::web::job_status::GenericInferenceJobStatus; use mysql_queries::queries::generic_inference::web::mark_generic_inference_job_cancelled_by_user::mark_generic_inference_job_cancelled_by_user; use mysql_queries::queries::generic_inference::web::mark_generic_inference_job_successfully_done_by_token::mark_generic_inference_job_successfully_done_by_token; -use mysql_queries::queries::media_files::create::generic_insert::insert_media_file_generic::{insert_media_file_generic, InsertArgs}; use mysql_queries::queries::media_files::create::insert_builder::media_file_insert_builder::MediaFileInsertBuilder; use mysql_queries::queries::media_files::create::specialized_insert::insert_media_file_from_file_upload::{insert_media_file_from_file_upload, InsertMediaFileFromUploadArgs, UploadType}; use mysql_queries::queries::tts::tts_inference_jobs::mark_tts_inference_job_permanently_dead::mark_tts_inference_job_permanently_dead; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/handle_model_mesh_payload.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/handle_model_mesh_payload.rs index 77951e5208..6cbf650f82 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/handle_model_mesh_payload.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/handle_model_mesh_payload.rs @@ -18,6 +18,7 @@ use mysql_queries::queries::generic_inference::fal::get_inference_job_by_fal_id: use mysql_queries::queries::media_files::create::insert_builder::media_file_insert_builder::MediaFileInsertBuilder; use serde_json::{Map, Value}; use std::sync::Arc; +use enums::by_table::media_files::media_file_engine_category::MediaFileEngineCategory; use tokens::tokens::media_files::MediaFileToken; const PREFIX : Option<&str> = Some("artcraft_"); @@ -90,6 +91,7 @@ pub async fn handle_model_mesh_payload( .media_file_class(MediaFileClass::Dimensional) .media_file_type(media_file_type) .media_file_origin_category(MediaFileOriginCategory::Inference) + .maybe_engine_category(Some(MediaFileEngineCategory::Object)) //.media_file_origin_product_category(MediaFileOriginProductCategory::Unknown) .mime_type(mime_type) .file_size_bytes(file_size_bytes as u64) diff --git a/frontend/clean_modules.sh b/frontend/clean_modules.sh index 6e69b5959f..5caa857ccd 100755 --- a/frontend/clean_modules.sh +++ b/frontend/clean_modules.sh @@ -14,6 +14,7 @@ rm -rf ./apps/editor2d/node_modules/ rm -rf ./apps/editor3d/node_modules/ echo "Removing Build directories..." +rm -rf ./apps/artcraft-website/dist/ rm -rf ./apps/editor2d/dist/ rm -rf ./apps/editor3d/dist/ rm -rf ./libs/api/dist/ diff --git a/frontend/libs/components/lightbox-modal/src/lib/lightbox-modal.tsx b/frontend/libs/components/lightbox-modal/src/lib/lightbox-modal.tsx index 2e1e7d7c57..773cbaf7f7 100644 --- a/frontend/libs/components/lightbox-modal/src/lib/lightbox-modal.tsx +++ b/frontend/libs/components/lightbox-modal/src/lib/lightbox-modal.tsx @@ -5,8 +5,9 @@ import { createPortal } from "react-dom"; import dayjs from "dayjs"; import { faDownToLine } from "@fortawesome/pro-solid-svg-icons"; import { - FalKlingImageToVideo, FalHunyuanImageTo3d, + EnqueueImageTo3dObject, + EnqueueImageTo3dObjectModel, } from "@storyteller/tauri-api"; interface LightboxModalProps { @@ -129,9 +130,13 @@ export function LightboxModal({