Compare commits

...
Author SHA1 Message Date
CrossPr0duct 617aa75dc5 Background removal race condition fixed. 2025-06-19 13:13:30 -04:00
BFlat 10603f7c86 feature/lightbox drag (#677)
- draggable and resizable lightbox

Bug found: if lightbox is open, gallery modal cannot open the filter
dropdown until lightbox is closed.
2025-06-19 00:41:21 +07:00
5e8c524f84 Feature/login fix (#678)
Co-authored-by: CrossPr0duct <alicextensor@gmail.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
2025-06-18 10:59:43 -04:00
Brandon Thomas 05f0fe1216 fal api endpoints in api gateway (#676) 2025-06-17 01:18:36 -04:00
ArEnScandCrossPr0duct 1f44f7e929 Feature/fixes (#675)
Co-authored-by: CrossPr0duct <alicextensor@gmail.com>
2025-06-16 23:58:35 -04:00
BFlat c56cc8700c gif tooltips for 2d and 3d (#674) 2025-06-16 14:49:02 +07:00
Brandon Thomas caaff569a6 fal client - lots of api wrappers (#673) 2025-06-16 01:08:27 -04:00
BFlat 3b0ed7a8ba feature: create 3d model modal (#668)
- Create 3d model from image works now (accessed via wand button next to
the + asset button).
- Modal pops up, or if you dont have a FAL api set in settings it'll pop
up an action reminder to set it first.
- You can choose image from library (opens gallery select mode) or
upload an image from computer.
- Pressing create enqueues the image to 3d and closes the modal.
Spinning loading toast on the top right says Generating... until it
finishes.
- When done, currently it just pops up in the library and you can drag
it onto the scene yourself.

**Current bugs/missing features:**
1. No 3D object image thumbnail in gallery
2. If you open up the gallery and try add the newly generated 3d object
to the scene, it will be stuck at "loading" text because of a CORS error
(not sure why). Pressing the button to refresh the gallery before
dragging it in seems to fix that though.
2025-06-14 20:44:03 -04:00
Brandon Thomas ea1905cfcf tauri bg removal fix (#672) 2025-06-14 01:25:26 -04:00
ArEnScandCrossPr0duct d1f28877ca Feature/add background remove (#670)
Co-authored-by: CrossPr0duct <alicextensor@gmail.com>
2025-06-13 19:07:41 -04:00
BFlat 1b8d8cbd97 login error messages now show properly (#671) 2025-06-13 17:33:36 +07:00
bflatastic 8be5013260 re-enable login modal 2025-06-13 16:29:09 +07:00
bflatastic 3c9b81c3ea bug fix: 3d promptbox events 2025-06-12 22:35:27 +07:00
BFlat 9d1b7d22ee bug fix: ensure no keyboard events leak through to the canvas (#669) 2025-06-12 22:30:43 +07:00
ArEnScandCrossPr0duct b046f6ff2a Feature/Zlayer and opacity issues (#667)
This adds all the z-layer work and context menus back for composition.
Also fixes the alpha issues and removes menus that are not needed for
now.

---------

Co-authored-by: CrossPr0duct <alicextensor@gmail.com>
2025-06-11 13:43:29 -04:00
BFlat df0b98ec63 drag and drop gallery 3d objects to 3d scene feature (#666)
- can now drag and drop 3d models onto the editor scene from the gallery
modal
2025-06-10 12:48:44 +07:00
BFlat f214577af1 2D canvas gallery image drop feature (#665)
- now you can drag and drop onto the 2D canvas from the gallery modal
2025-06-10 12:24:28 +07:00
ArEnScandCrossPr0duct 3efe9db4ca Create Menus and Bug Fixes (#664)
Removed:
Save and Load Scene for later.
Added undo and redo as button.
Hide Generate Image
Added a menu ( still needs logic code )
Fixed Opacity on brush.

---------

Co-authored-by: CrossPr0duct <alicextensor@gmail.com>
2025-06-09 17:36:16 -04:00
Brandon Thomas 8208206e5a missing 2025-06-09 12:20:21 -04:00
Tanish Manku 4f33e74e3a Make 3D state save and load on tab change (#661) 2025-06-06 22:50:47 -07:00
Brandon Thomas f75b210dcd end-to-end fal in artcraft backend (#663) 2025-06-06 21:37:06 -04:00
Brandon ThomasandMichael Chung 7a07f196d1 Fix Safari issue (#662)
Co-authored-by: Michael Chung <michaelchung@mac.home>
2025-06-06 20:26:30 -04:00
198 changed files with 10975 additions and 3032 deletions
@@ -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"
}
Generated
+1655 -1961
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -43,6 +43,7 @@ members = [
"crates/lib/data/crockford",
"crates/lib/data/datetimes",
"crates/lib/data/primitives",
"crates/lib/data/web_base64",
"crates/lib/deprecated/config", # TODO(bt, 2023-02-13): Deprecate or repurpose
"crates/lib/deprecated/http_server_common", # TODO(bt, 2023-03-20): Move useful pieces to actix_helpers
"crates/lib/dimensions",
@@ -228,3 +229,4 @@ shared_service_components = { path = "crates/lib/shared_service_components" }
storyteller_client = { path = "crates/lib/api_clients/storyteller_client" }
subprocess_common = { path = "crates/lib/subprocess_common" }
tokens = { path = "crates/schema/public/tokens" }
web_base64= { path = "crates/lib/data/web_base64" }
+3
View File
@@ -23,16 +23,19 @@ 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 }
primitives = { workspace = true }
storyteller_client = { workspace = true }
tokens = { workspace = true }
web_base64 = { workspace = true }
# External
anyhow = { workspace = true }
@@ -0,0 +1,48 @@
use std::error::Error;
use std::fmt::{Display, Formatter};
use storyteller_client::error::storyteller_error::StorytellerError;
#[derive(Debug)]
pub enum ArtcraftError {
AnyhowError(anyhow::Error),
DecodeError(base64::DecodeError),
IoError(std::io::Error),
StorytellerError(StorytellerError),
}
impl Error for ArtcraftError {}
impl Display for ArtcraftError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::AnyhowError(e) => write!(f, "AnyhowError: {:?}", e),
Self::DecodeError(e) => write!(f, "DecodeError: {:?}", e),
Self::IoError(e) => write!(f, "IoError: {:?}", e),
Self::StorytellerError(e) => write!(f, "StorytellerError: {:?}", e),
}
}
}
impl From<anyhow::Error> for ArtcraftError {
fn from(value: anyhow::Error) -> Self {
Self::AnyhowError(value)
}
}
impl From<base64::DecodeError> for ArtcraftError {
fn from(value: base64::DecodeError) -> Self {
Self::DecodeError(value)
}
}
impl From<std::io::Error> for ArtcraftError {
fn from(value: std::io::Error) -> Self {
Self::IoError(value)
}
}
impl From<StorytellerError> for ArtcraftError {
fn from(value: StorytellerError) -> Self {
Self::StorytellerError(value)
}
}
@@ -1,5 +1,5 @@
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::image::handle_image_fal::handle_image_fal;
use crate::core::commands::enqueue::image::handle_image_sora::handle_image_sora;
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
use crate::core::commands::response::shorthand::Response;
@@ -24,7 +24,7 @@ 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_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
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};
@@ -55,6 +55,8 @@ 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::handle_image_artcraft::handle_image_artcraft;
use crate::core::commands::enqueue::object::handle_object_artcraft::handle_object_artcraft;
#[derive(Deserialize)]
pub struct EnqueueTextToImageRequest {
@@ -93,6 +95,8 @@ pub enum EnqueueTextToImageErrorType {
NeedsFalApiKey,
/// Fal had an API error
FalError,
/// Needs to be logged into Artcraft
NeedsStorytellerCredentials,
}
@@ -172,13 +176,20 @@ pub async fn handle_request(
None => {
return Err(InternalImageError::NoModelSpecified);
}
Some(EnqueueTextToImageModel::FluxProUltra) | Some(EnqueueTextToImageModel::Recraft3) => {
handle_fal(&app, request, fal_creds_manager, fal_task_queue).await?;
},
Some(EnqueueTextToImageModel::GptImage1) => {
handle_sora(&app, request, sora_creds_manager, sora_task_queue).await?;
handle_image_sora(&app, request, sora_creds_manager, sora_task_queue).await?;
return Ok(());
}
_ => {
// Fall-through
}
};
if fal_creds_manager.has_apparent_api_token()? {
handle_image_fal(&app, request, fal_creds_manager, fal_task_queue).await?;
} else {
handle_image_artcraft(request, &app, app_data_root, storyteller_creds_manager).await?;
}
Ok(())
}
@@ -0,0 +1,97 @@
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageModel, 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, 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::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::image::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageRequest;
use artcraft_api_defs::generate::object::generate_hunyuan_2_image_to_3d::GenerateHunyuan2ImageTo3dRequest;
use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProImageToVideoRequest;
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};
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::image::generate_flux_pro_11_ultra_text_to_image::generate_flux_pro_11_ultra_text_to_image;
use storyteller_client::generate::object::generate_hunyuan2_image_to_3d::generate_hunyuan2_image_to_3d;
use storyteller_client::generate::video::generate_kling_16_pro_image_to_video::generate_kling_16_pro_image_to_video;
use storyteller_client::utils::api_host::ApiHost;
use tauri::{AppHandle, State};
pub async fn handle_image_artcraft(
request: EnqueueTextToImageRequest,
app: &AppHandle,
app_data_root: &AppDataRoot,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<(), InternalImageError> {
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::GenerateImage);
if let Err(err) = event.send(&app) {
error!("Failed to emit event: {:?}", err); // Fail open.
}
return Err(InternalImageError::NeedsStorytellerCredentials);
},
};
info!("Calling Artcraft text to image...");
let uuid_idempotency_token = generate_random_uuid();
let mut selected_model = None;
let job_token = match request.model {
None => {
return Err(InternalImageError::NoModelSpecified);
}
Some(EnqueueTextToImageModel::GptImage1) => {
return Err(InternalImageError::AnyhowError(anyhow!("wrong logic: artcraft is handling sora images")));
}
Some(EnqueueTextToImageModel::Recraft3) => {
return Err(InternalImageError::AnyhowError(anyhow!("not yet implemented in Artcraft")));
}
Some(EnqueueTextToImageModel::FluxProUltra) => {
info!("enqueue Flux Pro Ultra");
selected_model = Some(GenerationModel::FluxPro11Ultra);
let request = GenerateFluxPro11UltraTextToImageRequest {
uuid_idempotency_token,
prompt: request.prompt,
aspect_ratio: None,
num_images: None,
};
let result = generate_flux_pro_11_ultra_text_to_image(
&ApiHost::Storyteller,
Some(&creds),
request,
).await;
match result {
Ok(enqueued) => {
info!("Successfully enqueued Artcraft flux pro ultra text to image generation");
enqueued.inference_job_token
}
Err(err) => {
error!("Failed to use Artcraft flux pro ultra text to image generation: {:?}", err);
return Err(InternalImageError::StorytellerError(err));
}
}
}
};
Ok(())
}
@@ -12,12 +12,12 @@ 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::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_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
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_fal(
pub async fn handle_image_fal(
app: &AppHandle,
request: EnqueueTextToImageRequest,
fal_creds_manager: &FalCredentialManager,
@@ -56,7 +56,7 @@ pub async fn handle_fal(
}
Some(EnqueueTextToImageModel::FluxProUltra) => {
info!("enqueue Flux Pro Ultra text-to-image with prompt: {}", prompt);
enqueue_flux_pro_ultra_text_to_image(FluxProUltraTextToImageArgs {
enqueue_flux_pro_11_ultra_text_to_image(FluxPro11UltraTextToImageArgs {
prompt,
api_key: &api_key,
}).await
@@ -11,7 +11,7 @@ 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::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_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
use fal_client::requests::queue::image_gen::enqueue_recraft3_text_to_image::{enqueue_recraft3_text_to_image, Recraft3TextToImageArgs};
use log::{error, info, warn};
use openai_sora_client::creds::sora_credential_set::SoraCredentialSet;
@@ -25,7 +25,7 @@ use tauri::AppHandle;
const SORA_SIMPLE_IMAGE_GEN_TIMEOUT : Duration = Duration::from_millis(1000 * 30); // 30 seconds
pub async fn handle_sora(
pub async fn handle_image_sora(
app: &AppHandle,
request: EnqueueTextToImageRequest,
sora_creds_manager: &SoraCredentialManager,
@@ -1,20 +1,23 @@
use crate::core::artcraft_error::ArtcraftError;
use base64::DecodeError;
use errors::AnyhowError;
use fal_client::error::fal_error_plus::FalErrorPlus;
use openai_sora_client::sora_error::SoraError;
use storyteller_client::error::api_error::ApiError;
use storyteller_client::error::storyteller_error::StorytellerError;
#[derive(Debug)]
pub enum InternalImageError {
NoModelSpecified,
NeedsFalApiKey,
NeedsSoraCredentials,
NeedsStorytellerCredentials,
FalError(FalErrorPlus),
SoraError(SoraError),
AnyhowError(AnyhowError),
StorytellerApiError(ApiError),
StorytellerError(StorytellerError),
DecodeError(DecodeError),
IoError(std::io::Error),
ArtcraftError(ArtcraftError),
}
impl From<AnyhowError> for InternalImageError {
@@ -35,9 +38,9 @@ impl From<SoraError> for InternalImageError {
}
}
impl From<ApiError> for InternalImageError {
fn from(value: ApiError) -> Self {
Self::StorytellerApiError(value)
impl From<StorytellerError> for InternalImageError {
fn from(value: StorytellerError) -> Self {
Self::StorytellerError(value)
}
}
@@ -52,3 +55,15 @@ impl From<std::io::Error> for InternalImageError {
Self::IoError(value)
}
}
impl From<ArtcraftError> for InternalImageError {
fn from(value: ArtcraftError) -> Self {
match value {
ArtcraftError::AnyhowError(e) => Self::AnyhowError(e),
ArtcraftError::DecodeError(e) => Self::DecodeError(e),
ArtcraftError::IoError(e) => Self::IoError(e),
ArtcraftError::StorytellerError(e) => Self::StorytellerError(e),
_ => Self::ArtcraftError(value),
}
}
}
@@ -1,4 +1,5 @@
pub mod enqueue_text_to_image_command;
pub mod handle_fal;
pub mod handle_sora;
pub mod handle_image_artcraft;
pub mod handle_image_fal;
pub mod handle_image_sora;
pub mod internal_image_error;
@@ -1 +1,3 @@
pub mod image;
pub mod image;
pub mod object;
pub mod video;
@@ -0,0 +1,177 @@
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::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_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
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;
#[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<MediaFileToken>,
/// The model to use.
pub model: Option<EnqueueImageTo3dObjectModel>,
}
#[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<EnqueueImageTo3dObjectSuccessResponse, EnqueueImageTo3dObjectErrorType, ()> {
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(())
}
@@ -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_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
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(())
}
@@ -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_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
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::ImageTo3d);
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(())
}
@@ -0,0 +1,61 @@
use crate::core::artcraft_error::ArtcraftError;
use base64::DecodeError;
use errors::AnyhowError;
use fal_client::error::fal_error_plus::FalErrorPlus;
use storyteller_client::error::storyteller_error::StorytellerError;
#[derive(Debug)]
pub enum InternalObjectError {
NoModelSpecified,
NeedsFalApiKey,
NeedsSoraCredentials,
NeedsStorytellerCredentials,
FalError(FalErrorPlus),
AnyhowError(AnyhowError),
StorytellerError(StorytellerError),
DecodeError(DecodeError),
IoError(std::io::Error),
ArtcraftError(ArtcraftError),
}
impl From<AnyhowError> for InternalObjectError {
fn from(value: AnyhowError) -> Self {
Self::AnyhowError(value)
}
}
impl From<FalErrorPlus> for InternalObjectError {
fn from(value: FalErrorPlus) -> Self {
Self::FalError(value)
}
}
impl From<StorytellerError> for InternalObjectError {
fn from(value: StorytellerError) -> Self {
Self::StorytellerError(value)
}
}
impl From<DecodeError> for InternalObjectError {
fn from(value: DecodeError) -> Self {
Self::DecodeError(value)
}
}
impl From<std::io::Error> for InternalObjectError {
fn from(value: std::io::Error) -> Self {
Self::IoError(value)
}
}
impl From<ArtcraftError> for InternalObjectError {
fn from(value: ArtcraftError) -> Self {
match value {
ArtcraftError::AnyhowError(e) => Self::AnyhowError(e),
ArtcraftError::DecodeError(e) => Self::DecodeError(e),
ArtcraftError::IoError(e) => Self::IoError(e),
ArtcraftError::StorytellerError(e) => Self::StorytellerError(e),
_ => Self::ArtcraftError(value),
}
}
}
@@ -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;
@@ -0,0 +1,179 @@
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;
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_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
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;
#[derive(Deserialize)]
pub struct EnqueueImageToVideoRequest {
/// 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<MediaFileToken>,
/// The model to use.
pub model: Option<EnqueueImageToVideoModel>,
}
#[derive(Deserialize, Debug, Copy, Clone)]
#[serde(rename_all = "snake_case")]
pub enum EnqueueImageToVideoModel {
#[serde(rename = "kling1_6")]
Kling16,
}
#[derive(Serialize)]
pub struct EnqueueImageToVideoSuccessResponse {
}
impl SerializeMarker for EnqueueImageToVideoSuccessResponse {}
#[derive(Serialize, Debug)]
#[serde(rename_all = "snake_case")]
pub enum EnqueueImageToVideoErrorType {
/// 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_video_command(
app: AppHandle,
request: EnqueueImageToVideoRequest,
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<EnqueueImageToVideoSuccessResponse, EnqueueImageToVideoErrorType, ()> {
info!("enqueue_image_to_video_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 = EnqueueImageToVideoErrorType::ServerError;
let mut error_message = "A server error occurred. Please try again. If it continues, please tell our staff about the problem.";
match err {
InternalVideoError::NoModelSpecified => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueImageToVideoErrorType::ModelNotSpecified;
error_message = "No model specified for image generation";
}
InternalVideoError::NeedsFalApiKey => {
status = CommandErrorStatus::Unauthorized;
error_type = EnqueueImageToVideoErrorType::NeedsFalApiKey;
error_message = "You need to set a FAL api key";
},
InternalVideoError::NeedsStorytellerCredentials => {
status = CommandErrorStatus::Unauthorized;
error_type = EnqueueImageToVideoErrorType::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(EnqueueImageToVideoSuccessResponse {}.into())
}
}
}
pub async fn handle_request(
app: &AppHandle,
request: EnqueueImageToVideoRequest,
app_data_root: &AppDataRoot,
fal_creds_manager: &FalCredentialManager,
storyteller_creds_manager: &StorytellerCredentialManager,
fal_task_queue: &FalTaskQueue,
) -> Result<(), InternalVideoError> {
if fal_creds_manager.has_apparent_api_token()? {
handle_video_fal(&app, app_data_root, request, fal_creds_manager, fal_task_queue).await?;
} else {
handle_video_artcraft(request, &app, app_data_root, storyteller_creds_manager).await?;
}
Ok(())
}
@@ -0,0 +1,139 @@
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::{EnqueueImageToVideoModel, EnqueueImageToVideoRequest};
use crate::core::commands::enqueue::video::internal_video_error::InternalVideoError;
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::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 artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::{GenerateKling16ProAspectRatio, GenerateKling16ProImageToVideoRequest};
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};
use fal_client::requests::queue::image_gen::enqueue_recraft3_text_to_image::{enqueue_recraft3_text_to_image, Recraft3TextToImageArgs};
use fal_client::requests::queue::video_gen::enqueue_kling_16_pro_image_to_video::Kling16ProAspectRatio;
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::generate::video::generate_kling_16_pro_image_to_video::generate_kling_16_pro_image_to_video;
use storyteller_client::utils::api_host::ApiHost;
use tauri::{AppHandle, State};
pub async fn handle_video_artcraft(
request: EnqueueImageToVideoRequest,
app: &AppHandle,
app_data_root: &AppDataRoot,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<(), InternalVideoError> {
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::GenerateVideo);
if let Err(err) = event.send(&app) {
error!("Failed to emit event: {:?}", err); // Fail open.
}
return Err(InternalVideoError::NeedsStorytellerCredentials);
},
};
//let api_key = fal_creds_manager.get_key_required()
// .map_err(|err| {
// error!("EnqueueTextToImage FAL api key required: {:?}", err);
// InternalVideoError::NeedsFalApiKey
// })?;
info!("Calling Artcraft image to video...");
let uuid_idempotency_token = generate_random_uuid();
let mut selected_model = None;
let job_token = match request.model {
None => {
return Err(InternalVideoError::NoModelSpecified);
}
Some(EnqueueImageToVideoModel::Kling16) => {
info!("enqueue Kling 1.6");
selected_model = Some(GenerationModel::Kling1_6);
let request = GenerateKling16ProImageToVideoRequest {
uuid_idempotency_token,
media_file_token: request.image_media_token,
aspect_ratio: Some(GenerateKling16ProAspectRatio::WideSixteenNine),
prompt: None,
duration: None,
};
let result = generate_kling_16_pro_image_to_video(
&ApiHost::Storyteller,
Some(&creds),
request,
).await;
match result {
Ok(enqueued) => {
info!("Successfully enqueued Artcraft Kling 1.6 video generation");
enqueued.inference_job_token
}
Err(err) => {
error!("Failed to use Artcraft Kling 1.6 video generation: {:?}", err);
return Err(InternalVideoError::StorytellerError(err));
}
}
}
};
info!("Successfully enqueued video generation");
let event = GenerationEnqueueSuccessEvent {
action: GenerationAction::GenerateVideo,
service: GenerationServiceProvider::Artcraft,
model: selected_model,
};
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(InternalVideoError::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(InternalVideoError::FalError(err));
// }
// }
Ok(())
}
@@ -0,0 +1,124 @@
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageModel, EnqueueTextToImageRequest};
use crate::core::commands::enqueue::video::enqueue_image_to_video_command::{EnqueueImageToVideoModel, EnqueueImageToVideoRequest};
use crate::core::commands::enqueue::video::internal_video_error::InternalVideoError;
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::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_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
use fal_client::requests::queue::image_gen::enqueue_recraft3_text_to_image::{enqueue_recraft3_text_to_image, Recraft3TextToImageArgs};
use fal_client::requests::queue::video_gen::enqueue_kling_16_pro_image_to_video::{enqueue_kling_16_pro_image_to_video, Kling16ProArgs, Kling16ProAspectRatio, Kling16ProDuration};
use log::{error, info, warn};
use tauri::AppHandle;
pub async fn handle_video_fal(
app: &AppHandle,
app_data_root: &AppDataRoot,
request: EnqueueImageToVideoRequest,
fal_creds_manager: &FalCredentialManager,
fal_task_queue: &FalTaskQueue,
) -> Result<(), InternalVideoError> {
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::GenerateVideo);
if let Err(err) = event.send(&app) {
error!("Failed to emit event: {:?}", err); // Fail open.
}
return Err(InternalVideoError::NeedsFalApiKey);
},
};
//let api_key = fal_creds_manager.get_key_required()
// .map_err(|err| {
// error!("EnqueueTextToImage FAL api key required: {:?}", err);
// InternalVideoError::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(InternalVideoError::AnyhowError(anyhow!("No image media token provided")));
}
info!("Calling FAL image to 3d ...");
let filename = temp_download.path().to_path_buf();
let mut selected_model = None;
let result = match request.model {
None => {
return Err(InternalVideoError::NoModelSpecified);
}
Some(EnqueueImageToVideoModel::Kling16) => {
info!("enqueue Kling 1.6 image to video");
selected_model = Some(GenerationModel::Kling1_6);
enqueue_kling_16_pro_image_to_video(Kling16ProArgs {
image_path: filename,
api_key: &api_key,
duration: Kling16ProDuration::Default,
aspect_ratio: Kling16ProAspectRatio::WideSixteenNine,
prompt: "",
}).await
}
};
match result {
Ok(enqueued) => {
info!("Successfully enqueued Kling 1.6 image to video");
let event = GenerationEnqueueSuccessEvent {
action: GenerationAction::GenerateVideo,
service: GenerationServiceProvider::Fal,
model: selected_model,
};
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(InternalVideoError::AnyhowError(anyhow!("Failed to enqueue task: {:?}", err)));
}
}
Err(err) => {
error!("Failed to enqueue image to video: {:?}", err);
let event = GenerationEnqueueFailureEvent {
action: GenerationAction::GenerateVideo,
service: GenerationServiceProvider::Fal,
model: selected_model,
reason: None,
};
if let Err(err) = event.send(app) {
error!("Failed to emit event: {:?}", err); // Fail open.
}
return Err(InternalVideoError::FalError(err));
}
}
Ok(())
}
@@ -0,0 +1,61 @@
use crate::core::artcraft_error::ArtcraftError;
use base64::DecodeError;
use errors::AnyhowError;
use fal_client::error::fal_error_plus::FalErrorPlus;
use storyteller_client::error::storyteller_error::StorytellerError;
#[derive(Debug)]
pub enum InternalVideoError {
NoModelSpecified,
NeedsFalApiKey,
NeedsSoraCredentials,
NeedsStorytellerCredentials,
FalError(FalErrorPlus),
AnyhowError(AnyhowError),
StorytellerError(StorytellerError),
DecodeError(DecodeError),
IoError(std::io::Error),
ArtcraftError(ArtcraftError),
}
impl From<AnyhowError> for InternalVideoError {
fn from(value: AnyhowError) -> Self {
Self::AnyhowError(value)
}
}
impl From<FalErrorPlus> for InternalVideoError {
fn from(value: FalErrorPlus) -> Self {
Self::FalError(value)
}
}
impl From<StorytellerError> for InternalVideoError {
fn from(value: StorytellerError) -> Self {
Self::StorytellerError(value)
}
}
impl From<DecodeError> for InternalVideoError {
fn from(value: DecodeError) -> Self {
Self::DecodeError(value)
}
}
impl From<std::io::Error> for InternalVideoError {
fn from(value: std::io::Error) -> Self {
Self::IoError(value)
}
}
impl From<ArtcraftError> for InternalVideoError {
fn from(value: ArtcraftError) -> Self {
match value {
ArtcraftError::AnyhowError(e) => Self::AnyhowError(e),
ArtcraftError::DecodeError(e) => Self::DecodeError(e),
ArtcraftError::IoError(e) => Self::IoError(e),
ArtcraftError::StorytellerError(e) => Self::StorytellerError(e),
_ => Self::ArtcraftError(value),
}
}
}
@@ -0,0 +1,4 @@
pub mod enqueue_image_to_video_command;
pub mod handle_video_artcraft;
pub mod handle_video_fal;
pub mod internal_video_error;
@@ -3,13 +3,16 @@ 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)]
#[serde(rename_all = "snake_case")]
pub enum GenerationModel {
#[serde(rename = "flux_pro_1.1_ultra")]
FluxPro11Ultra,
#[serde(rename = "kling_1.6")]
Kling1_6,
#[serde(rename = "kling_2.0")]
@@ -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,
+1
View File
@@ -1,3 +1,4 @@
pub mod artcraft_error;
pub mod commands;
pub mod events;
pub mod state;
@@ -33,6 +33,7 @@ pub struct AppDataRoot {
impl AppDataRoot {
pub fn create_default() -> anyhow::Result<Self> {
let directory = get_default_data_dir()?;
println!("App data directory: {:?}", directory);
Self::create_existing(directory)
}
@@ -1,7 +1,7 @@
use crate::core::artcraft_error::ArtcraftError;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::utils::get_url_file_extension::get_url_file_extension;
use crate::core::utils::simple_http_download_to_tempfile::simple_http_download_to_tempfile;
use errors::AnyhowResult;
use storyteller_client::media_files::get_media_file::get_media_file;
use storyteller_client::utils::api_host::ApiHost;
use tempfile::NamedTempFile;
@@ -9,7 +9,7 @@ use tokens::tokens::media_files::MediaFileToken;
// TODO: Better more concrete error handling
pub async fn download_media_file_to_temp_dir(app_data_root: &AppDataRoot, token: &MediaFileToken) -> AnyhowResult<NamedTempFile> {
pub async fn download_media_file_to_temp_dir(app_data_root: &AppDataRoot, token: &MediaFileToken) -> Result<NamedTempFile, ArtcraftError> {
let response = get_media_file(&ApiHost::Storyteller, token).await?;
let media_file_url = &response.media_file.media_links.cdn_url;
@@ -1,15 +1,12 @@
use crate::core::artcraft_error::ArtcraftError;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use base64::prelude::BASE64_STANDARD;
use base64::Engine;
use errors::AnyhowResult;
use mimetypes::mimetype_info::mimetype_info::MimetypeInfo;
use std::io::Write;
use tempfile::NamedTempFile;
use web_base64::web_base64_decode::web_base64_decode;
// TODO: Retain error types (io/decode)
pub async fn save_base64_image_to_temp_dir(app_data_root: &AppDataRoot, base64_image: String) -> AnyhowResult<NamedTempFile> {
let bytes = BASE64_STANDARD.decode(base64_image)?;
pub async fn save_base64_image_to_temp_dir(app_data_root: &AppDataRoot, base64_image: String) -> Result<NamedTempFile, ArtcraftError> {
let bytes = web_base64_decode(&base64_image)?;
let extension = MimetypeInfo::get_for_bytes(&bytes)
.map(|info| info.file_extension())
+4
View File
@@ -6,6 +6,8 @@ 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::enqueue::video::enqueue_image_to_video_command::enqueue_image_to_video_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 +163,8 @@ pub fn run() {
.manage(storyteller_creds_manager_3)
.invoke_handler(tauri::generate_handler![
check_sora_session_command,
enqueue_image_to_3d_object_command,
enqueue_image_to_video_command,
enqueue_text_to_image_command,
fal_background_removal_command,
fal_hunyuan_image_to_3d_command,
@@ -1,3 +1,4 @@
use crate::core::artcraft_error::ArtcraftError;
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;
@@ -49,6 +50,7 @@ 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::error::storyteller_error::StorytellerError;
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;
@@ -176,9 +178,10 @@ pub async fn fal_background_removal_command(
enum InnerError {
FalError(FalErrorPlus),
AnyhowError(AnyhowError),
StorytellerApiError(ApiError),
StorytellerError(StorytellerError),
DecodeError(DecodeError),
IoError(std::io::Error),
ArtcraftError(ArtcraftError),
}
impl From<AnyhowError> for InnerError {
@@ -193,9 +196,9 @@ impl From<FalErrorPlus> for InnerError {
}
}
impl From<ApiError> for InnerError {
fn from(value: ApiError) -> Self {
Self::StorytellerApiError(value)
impl From<StorytellerError> for InnerError {
fn from(value: StorytellerError) -> Self {
Self::StorytellerError(value)
}
}
@@ -211,6 +214,17 @@ impl From<std::io::Error> for InnerError {
}
}
impl From<ArtcraftError> for InnerError {
fn from(value: ArtcraftError) -> Self {
match value {
ArtcraftError::AnyhowError(e) => Self::AnyhowError(e),
ArtcraftError::DecodeError(e) => Self::DecodeError(e),
ArtcraftError::IoError(e) => Self::IoError(e),
_ => Self::ArtcraftError(value),
}
}
}
pub async fn remove_background(
request: FalBackgroundRemovalRequest,
app_data_root: &AppDataRoot,
@@ -224,9 +238,11 @@ pub async fn remove_background(
let mut temp_download;
if let Some(media_token) = request.image_media_token {
info!("From media token: {:?}", &media_token);
temp_download = download_media_file_to_temp_dir(&app_data_root, &media_token).await?;
} else if let Some(base64_bytes) = request.base64_image {
info!("From base64 bytes...");
temp_download = save_base64_image_to_temp_dir(&app_data_root, base64_bytes).await?;
} else {
return Err(InnerError::AnyhowError(anyhow!("No image media token or base64 image provided")));
@@ -237,6 +253,8 @@ pub async fn remove_background(
let filename = temp_download.path().to_path_buf();
let result = remove_background_rembg(filename, &api_key).await?;
info!("Unpacking FAL result...");
let extension_with_dot = get_url_file_extension(&result.image_url)
.map(|ext| format!(".{}", ext))
@@ -1,3 +1,4 @@
use crate::core::artcraft_error::ArtcraftError;
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
use crate::core::commands::response::shorthand::Response;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
@@ -49,6 +50,7 @@ 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::error::storyteller_error::StorytellerError;
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;
@@ -152,9 +154,10 @@ pub async fn fal_hunyuan_image_to_3d_command(
enum InnerError {
FalError(FalErrorPlus),
AnyhowError(AnyhowError),
StorytellerApiError(ApiError),
StorytellerError(StorytellerError),
DecodeError(DecodeError),
IoError(std::io::Error),
ArtcraftError(ArtcraftError),
}
impl From<AnyhowError> for InnerError {
@@ -169,9 +172,9 @@ impl From<FalErrorPlus> for InnerError {
}
}
impl From<ApiError> for InnerError {
fn from(value: ApiError) -> Self {
Self::StorytellerApiError(value)
impl From<StorytellerError> for InnerError {
fn from(value: StorytellerError) -> Self {
Self::StorytellerError(value)
}
}
@@ -187,6 +190,19 @@ impl From<std::io::Error> for InnerError {
}
}
impl From<ArtcraftError> for InnerError {
fn from(value: ArtcraftError) -> Self {
match value {
ArtcraftError::AnyhowError(e) => Self::AnyhowError(e),
ArtcraftError::DecodeError(e) => Self::DecodeError(e),
ArtcraftError::IoError(e) => Self::IoError(e),
ArtcraftError::StorytellerError(e) => Self::StorytellerError(e),
_ => Self::ArtcraftError(value),
}
}
}
pub async fn image_to_3d(
request: FalHunyuanImageTo3dRequest,
app_data_root: &AppDataRoot,
@@ -196,7 +212,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;
@@ -1,3 +1,4 @@
use crate::core::artcraft_error::ArtcraftError;
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
use crate::core::commands::response::shorthand::Response;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
@@ -24,7 +25,7 @@ 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::video_gen::enqueue_kling_16_image_to_video::{enqueue_kling_16_image_to_video, Kling16Args, Kling16Duration};
use fal_client::requests::queue::video_gen::enqueue_kling_16_pro_image_to_video::{enqueue_kling_16_pro_image_to_video, Kling16ProArgs, Kling16ProAspectRatio, Kling16ProDuration};
use filesys::file_read_bytes::file_read_bytes;
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
use image::{DynamicImage, EncodableLayout, ImageReader};
@@ -49,6 +50,7 @@ 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::error::storyteller_error::StorytellerError;
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;
@@ -157,9 +159,10 @@ pub async fn fal_kling_image_to_video_command(
enum InnerError {
FalError(FalErrorPlus),
AnyhowError(AnyhowError),
StorytellerApiError(ApiError),
StorytellerError(StorytellerError),
DecodeError(DecodeError),
IoError(std::io::Error),
ArtcraftError(ArtcraftError),
}
impl From<AnyhowError> for InnerError {
@@ -174,9 +177,9 @@ impl From<FalErrorPlus> for InnerError {
}
}
impl From<ApiError> for InnerError {
fn from(value: ApiError) -> Self {
Self::StorytellerApiError(value)
impl From<StorytellerError> for InnerError {
fn from(value: StorytellerError) -> Self {
Self::StorytellerError(value)
}
}
@@ -192,6 +195,18 @@ impl From<std::io::Error> for InnerError {
}
}
impl From<ArtcraftError> for InnerError {
fn from(value: ArtcraftError) -> Self {
match value {
ArtcraftError::AnyhowError(e) => Self::AnyhowError(e),
ArtcraftError::DecodeError(e) => Self::DecodeError(e),
ArtcraftError::IoError(e) => Self::IoError(e),
ArtcraftError::StorytellerError(e) => Self::StorytellerError(e),
_ => Self::ArtcraftError(value),
}
}
}
pub async fn image_to_video(
request: FalKlingImageToVideoRequest,
app_data_root: &AppDataRoot,
@@ -218,10 +233,11 @@ pub async fn image_to_video(
let filename = temp_download.path().to_path_buf();
let enqueued = enqueue_kling_16_image_to_video(Kling16Args {
let enqueued = enqueue_kling_16_pro_image_to_video(Kling16ProArgs {
image_path: filename,
api_key: &api_key,
duration: Kling16Duration::Default,
duration: Kling16ProDuration::Default,
aspect_ratio: Kling16ProAspectRatio::WideSixteenNine,
prompt: request.prompt.as_deref().unwrap_or(""),
}).await?;
@@ -2,4 +2,6 @@ pub mod fal_background_removal_command;
pub mod fal_hunyuan_image_to_3d_command;
pub mod fal_kling_image_to_video_command;
pub mod get_fal_api_key_command;
pub mod set_fal_api_key_command;
pub mod set_fal_api_key_command;
// pub mod get_open_ai_api_key_command;
// pub mod set_open_ai_api_key_command;
@@ -39,6 +39,7 @@ use storyteller_client::error::api_error::ApiError::InternalServerError;
use storyteller_client::media_files::get_media_file::get_media_file;
use storyteller_client::utils::api_host::ApiHost;
use tauri::{AppHandle, Emitter, Manager, State};
use storyteller_client::error::storyteller_error::StorytellerError;
use tokens::tokens::media_files::MediaFileToken;
const SORA_IMAGE_UPLOAD_TIMEOUT: Duration = Duration::from_millis(1000 * 30); // 30 seconds
@@ -199,7 +200,7 @@ pub async fn sora_image_remix_command(
enum InnerError {
SoraError(SoraError),
AnyhowError(AnyhowError),
StorytellerApiError(ApiError),
StorytellerError(StorytellerError),
}
impl From<AnyhowError> for InnerError {
@@ -214,9 +215,9 @@ impl From<SoraError> for InnerError {
}
}
impl From<ApiError> for InnerError {
fn from(value: ApiError) -> Self {
Self::StorytellerApiError(value)
impl From<StorytellerError> for InnerError {
fn from(value: StorytellerError) -> Self {
Self::StorytellerError(value)
}
}
+3 -2
View File
@@ -16,7 +16,8 @@
"width": 2400,
"height": 1300,
"resizable": true,
"fullscreen": false
"fullscreen": false,
"dragDropEnabled": false
}
],
"security": {
@@ -33,7 +34,7 @@
"https://www.googletagmanager.com",
"ipc://localhost",
"tauri://localhost"
],
],
"connect-src": [
"'self'",
"'unsafe-eval'",
@@ -23,6 +23,7 @@ errors = { workspace = true }
# External
anyhow = { workspace = true }
futures = { workspace = true }
log = { workspace = true }
reqwest = { workspace = true }
serde = { workspace = true }
tokio = { workspace = true }
@@ -7,22 +7,25 @@ use fal::endpoints::fal_ai::flux_pro::v1_1_ultra::FluxProUltraTextToImageInput;
use futures::StreamExt;
use std::io::Write;
pub struct FluxProUltraTextToImageArgs<'a> {
pub struct FluxPro11UltraTextToImageArgs<'a> {
pub prompt: &'a str,
pub api_key: &'a FalApiKey,
// TODO: Size parameter
}
pub async fn enqueue_flux_pro_ultra_text_to_image(args: FluxProUltraTextToImageArgs<'_>) -> Result<EnqueuedRequest, FalErrorPlus> {
pub async fn enqueue_flux_pro_11_ultra_text_to_image(args: FluxPro11UltraTextToImageArgs<'_>) -> Result<EnqueuedRequest, FalErrorPlus> {
let request = FluxProUltraTextToImageInput {
prompt: args.prompt.to_string(),
safety_tolerance: Some("5".to_string()), // 1 is most strict, 5 is most permissive
enable_safety_checker: Some(false),
num_images: Some(1), // Default is 1
output_format: Some("png".to_string()),
raw: Some(true), // Generate less processed, more natural-looking images. Default is false.
// Maybe expose
aspect_ratio: None, // Default is "16:9"
num_images: Some(1), // Default is 1
seed: None,
raw: Some(true), // Generate less processed, more natural-looking images. Default is false.
// Maybe abstract
enable_safety_checker: Some(false),
safety_tolerance: Some("5".to_string()), // 1 is most strict, 5 is most permissive
// Constants
output_format: Some("png".to_string()),
sync_mode: None, // Synchronous / slow
};
@@ -42,7 +45,7 @@ pub async fn enqueue_flux_pro_ultra_text_to_image(args: FluxProUltraTextToImageA
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::queue::image_gen::enqueue_flux_pro_ultra_text_to_image::{enqueue_flux_pro_ultra_text_to_image, FluxProUltraTextToImageArgs};
use crate::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
use errors::AnyhowResult;
use std::fs::read_to_string;
@@ -54,12 +57,12 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = FluxProUltraTextToImageArgs {
let args = FluxPro11UltraTextToImageArgs {
prompt: "a corgi smiles at the camera, inside retro video game store, lots of sci-fi props from Cowboy Bebop, photorealistic",
api_key: &api_key,
};
let result = enqueue_flux_pro_ultra_text_to_image(args).await?;
let result = enqueue_flux_pro_11_ultra_text_to_image(args).await?;
Ok(())
}
@@ -1,2 +1,2 @@
pub mod enqueue_flux_pro_ultra_text_to_image;
pub mod enqueue_flux_pro_11_ultra_text_to_image;
pub mod enqueue_recraft3_text_to_image;
@@ -22,7 +22,7 @@ pub async fn remove_background_rembg<P: AsRef<Path>>(image_path: P, api_key: &Fa
crop_to_bbox: None,
sync_mode: None
};
let result = rembg(request)
.with_api_key(&api_key.0)
.queue()
@@ -56,6 +56,7 @@ mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::queue::remove_background_rembg::remove_background_rembg;
use errors::AnyhowResult;
use std::fs::read_to_string;
use testing::test_file_path::test_file_path;
#[tokio::test]
@@ -63,9 +64,9 @@ mod tests {
async fn test_remove_background_rembg() -> AnyhowResult<()> {
let image = test_file_path("test_data/image/juno.jpg")?;
// TODO: DO NOT COMMIT SECRET !
const KEY : &str = "DO NOT COMMIT";
let api_key = FalApiKey::from_str(KEY);
// XXX: Don't commit secrets!
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let result = remove_background_rembg(image, &api_key).await?;
@@ -8,35 +8,50 @@ use futures::StreamExt;
use std::io::Write;
use std::path::Path;
pub struct Kling16Args<'a, P: AsRef<Path>> {
pub struct Kling16ProArgs<'a, P: AsRef<Path>> {
pub image_path: P,
pub prompt: &'a str,
pub api_key: &'a FalApiKey,
pub duration: Kling16Duration,
pub duration: Kling16ProDuration,
pub aspect_ratio: Kling16ProAspectRatio,
}
#[derive(Copy, Clone, Debug)]
pub enum Kling16Duration {
pub enum Kling16ProDuration {
Default,
FiveSeconds,
TenSeconds,
}
pub async fn enqueue_kling_16_image_to_video<P: AsRef<Path>>(args: Kling16Args<'_, P>) -> Result<EnqueuedRequest, FalErrorPlus> {
#[derive(Copy, Clone, Debug)]
pub enum Kling16ProAspectRatio {
Square, // 1:1
WideSixteenNine, // 16:9
TallNineSixteen, // 9:16
}
pub async fn enqueue_kling_16_pro_image_to_video<P: AsRef<Path>>(args: Kling16ProArgs<'_, P>) -> Result<EnqueuedRequest, FalErrorPlus> {
let image_url = file_to_base64_url(args.image_path)?;
let duration = match args.duration{
Kling16Duration::Default => None,
Kling16Duration::FiveSeconds => Some("5".to_string()), // Gross...
Kling16Duration::TenSeconds => Some("10".to_string()),
Kling16ProDuration::Default => None,
Kling16ProDuration::FiveSeconds => Some("5".to_string()), // Gross...
Kling16ProDuration::TenSeconds => Some("10".to_string()),
};
let aspect_ratio = match args.aspect_ratio {
Kling16ProAspectRatio::Square => Some("1:1".to_string()),
Kling16ProAspectRatio::WideSixteenNine => Some("16:9".to_string()),
Kling16ProAspectRatio::TallNineSixteen => Some("9:16".to_string()),
};
let request = ProImageToVideoRequest {
image_url,
prompt: args.prompt.to_string(),
aspect_ratio: None,
cfg_scale: None,
aspect_ratio,
duration,
// Maybe expose these later
cfg_scale: None,
negative_prompt: None,
tail_image_url: None,
};
@@ -57,7 +72,7 @@ pub async fn enqueue_kling_16_image_to_video<P: AsRef<Path>>(args: Kling16Args<'
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::queue::video_gen::enqueue_kling_16_image_to_video::{enqueue_kling_16_image_to_video, Kling16Args, Kling16Duration};
use crate::requests::queue::video_gen::enqueue_kling_16_pro_image_to_video::{enqueue_kling_16_pro_image_to_video, Kling16ProArgs, Kling16ProAspectRatio, Kling16ProDuration};
use errors::AnyhowResult;
use std::fs::read_to_string;
use testing::test_file_path::test_file_path;
@@ -72,14 +87,15 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = Kling16Args {
let args = Kling16ProArgs {
image_path: image,
prompt: "a corgi looks out over the water",
api_key: &api_key,
duration: Kling16Duration::Default,
duration: Kling16ProDuration::Default,
aspect_ratio: Kling16ProAspectRatio::Square,
};
let result = enqueue_kling_16_image_to_video(args).await?;
let result = enqueue_kling_16_pro_image_to_video(args).await?;
Ok(())
}
@@ -1,2 +1,2 @@
pub mod enqueue_kling_16_image_to_video;
pub mod enqueue_kling_16_pro_image_to_video;
pub mod enqueue_minimax_01_image_to_video;
@@ -0,0 +1,106 @@
use crate::creds::fal_api_key::FalApiKey;
use crate::error::classify_fal_error::classify_fal_error;
use crate::error::fal_error_plus::FalErrorPlus;
use fal::endpoints::fal_ai::flux::dev::dev;
use fal::endpoints::fal_ai::flux::dev::DevTextToImageInput;
use fal::prelude::fal_ai::flux::dev::ImageSizeProperty;
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct Flux1DevArgs<'a, U: IntoUrl> {
pub prompt: &'a str,
pub webhook_url: U,
pub api_key: &'a FalApiKey,
pub aspect_ratio: Flux1DevAspectRatio,
pub num_images: Flux1DevNumImages,
}
#[derive(Copy, Clone, Debug)]
pub enum Flux1DevAspectRatio {
Square, // 1:1
SquareHd, // 1:1
LandscapeFourByThree, // 4:3
LandscapeSixteenByNine, // 16:9
PortraitThreeByFour, // 3:4
PortraitNineBySixteen, // 9:16
//Custom { width: u32, height: u32 }, // TODO
}
#[derive(Copy, Clone, Debug)]
pub enum Flux1DevNumImages {
One, // Default
Two,
Three,
Four,
}
pub async fn enqueue_flux_1_dev_text_to_image_webhook<U: IntoUrl>(
args: Flux1DevArgs<'_, U>
) -> Result<WebhookResponse, FalErrorPlus> {
let num_images = match args.num_images {
Flux1DevNumImages::One => 1,
Flux1DevNumImages::Two => 2,
Flux1DevNumImages::Three => 3,
Flux1DevNumImages::Four => 4,
};
let image_size = match args.aspect_ratio {
Flux1DevAspectRatio::Square => ImageSizeProperty::Square,
Flux1DevAspectRatio::SquareHd => ImageSizeProperty::SquareHd,
Flux1DevAspectRatio::LandscapeFourByThree => ImageSizeProperty::Landscape43,
Flux1DevAspectRatio::LandscapeSixteenByNine => ImageSizeProperty::Landscape169,
Flux1DevAspectRatio::PortraitThreeByFour => ImageSizeProperty::Portrait43,
Flux1DevAspectRatio::PortraitNineBySixteen => ImageSizeProperty::Portrait169,
};
let request = DevTextToImageInput {
prompt: args.prompt.to_string(),
num_images: Some(num_images),
image_size: Some(image_size),
// Maybe abstract
enable_safety_checker: Some(false),
// Maybe expose
seed: None,
guidance_scale: None,
num_inference_steps: None,
// Constants
sync_mode: None, // Synchronous / slow
};
let result = dev(request)
.with_api_key(&args.api_key.0)
.queue_webhook(args.webhook_url)
.await;
result.map_err(|err| classify_fal_error(err))
}
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::image::enqueue_flux_1_dev_text_to_image_webhook::{enqueue_flux_1_dev_text_to_image_webhook, Flux1DevArgs, Flux1DevAspectRatio, Flux1DevNumImages};
use errors::AnyhowResult;
use std::fs::read_to_string;
#[tokio::test]
#[ignore]
async fn test_flux_1_dev() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = Flux1DevArgs {
prompt: "a giant robot fighting a dragon in a futuristic city",
api_key: &api_key,
webhook_url: "https://example.com/webhook",
num_images: Flux1DevNumImages::One,
aspect_ratio: Flux1DevAspectRatio::LandscapeFourByThree
};
let result = enqueue_flux_1_dev_text_to_image_webhook(args).await?;
Ok(())
}
}
@@ -0,0 +1,105 @@
use crate::creds::fal_api_key::FalApiKey;
use crate::error::classify_fal_error::classify_fal_error;
use crate::error::fal_error_plus::FalErrorPlus;
use fal::endpoints::fal_ai::flux::schnell::schnell;
use fal::endpoints::fal_ai::flux::schnell::SchnellTextToImageInput;
use fal::prelude::fal_ai::flux::schnell::ImageSizeProperty;
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct Flux1SchnellArgs<'a, U: IntoUrl> {
pub prompt: &'a str,
pub webhook_url: U,
pub api_key: &'a FalApiKey,
pub aspect_ratio: Flux1SchnellAspectRatio,
pub num_images: Flux1SchnellNumImages,
}
#[derive(Copy, Clone, Debug)]
pub enum Flux1SchnellAspectRatio {
Square, // 1:1
SquareHd, // 1:1
LandscapeFourByThree, // 4:3
LandscapeSixteenByNine, // 16:9
PortraitThreeByFour, // 3:4
PortraitNineBySixteen, // 9:16
//Custom { width: u32, height: u32 }, // TODO
}
#[derive(Copy, Clone, Debug)]
pub enum Flux1SchnellNumImages {
One, // Default
Two,
Three,
Four,
}
pub async fn enqueue_flux_1_schnell_text_to_image_webhook<U: IntoUrl>(
args: Flux1SchnellArgs<'_, U>
) -> Result<WebhookResponse, FalErrorPlus> {
let num_images = match args.num_images {
Flux1SchnellNumImages::One => 1,
Flux1SchnellNumImages::Two => 2,
Flux1SchnellNumImages::Three => 3,
Flux1SchnellNumImages::Four => 4,
};
let image_size = match args.aspect_ratio {
Flux1SchnellAspectRatio::Square => ImageSizeProperty::Square,
Flux1SchnellAspectRatio::SquareHd => ImageSizeProperty::SquareHd,
Flux1SchnellAspectRatio::LandscapeFourByThree => ImageSizeProperty::Landscape43,
Flux1SchnellAspectRatio::LandscapeSixteenByNine => ImageSizeProperty::Landscape169,
Flux1SchnellAspectRatio::PortraitThreeByFour => ImageSizeProperty::Portrait43,
Flux1SchnellAspectRatio::PortraitNineBySixteen => ImageSizeProperty::Portrait169,
};
let request = SchnellTextToImageInput {
prompt: args.prompt.to_string(),
num_images: Some(num_images),
image_size: Some(image_size),
// Maybe abstract
enable_safety_checker: Some(false),
// Maybe expose
seed: None,
num_inference_steps: None,
// Constants
sync_mode: None, // Synchronous / slow
};
let result = schnell(request)
.with_api_key(&args.api_key.0)
.queue_webhook(args.webhook_url)
.await;
result.map_err(|err| classify_fal_error(err))
}
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::image::enqueue_flux_1_schnell_text_to_image_webhook::{enqueue_flux_1_schnell_text_to_image_webhook, Flux1SchnellArgs, Flux1SchnellAspectRatio, Flux1SchnellNumImages};
use errors::AnyhowResult;
use std::fs::read_to_string;
#[tokio::test]
#[ignore]
async fn test_flux_1_schnell() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = Flux1SchnellArgs {
prompt: "a giant robot fighting a dragon in a futuristic city",
api_key: &api_key,
webhook_url: "https://example.com/webhook",
num_images: Flux1SchnellNumImages::One,
aspect_ratio: Flux1SchnellAspectRatio::LandscapeSixteenByNine,
};
let result = enqueue_flux_1_schnell_text_to_image_webhook(args).await?;
Ok(())
}
}
@@ -0,0 +1,104 @@
use crate::creds::fal_api_key::FalApiKey;
use crate::error::classify_fal_error::classify_fal_error;
use crate::error::fal_error_plus::FalErrorPlus;
use fal::endpoints::fal_ai::flux_pro::v1_1::{v1_1, FluxProPlusTextToImageInput, ImageSizeProperty};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct FluxPro11Args<'a, U: IntoUrl> {
pub prompt: &'a str,
pub webhook_url: U,
pub api_key: &'a FalApiKey,
pub aspect_ratio: FluxPro11AspectRatio,
pub num_images: FluxPro11NumImages,
}
#[derive(Copy, Clone, Debug)]
pub enum FluxPro11AspectRatio {
Square, // 1:1
SquareHd, // 1:1
LandscapeFourByThree, // 4:3
LandscapeSixteenByNine, // 16:9
PortraitThreeByFour, // 3:4
PortraitNineBySixteen, // 9:16
//Custom { width: u32, height: u32 }, // TODO
}
#[derive(Copy, Clone, Debug)]
pub enum FluxPro11NumImages {
One, // Default
Two,
Three,
Four,
}
pub async fn enqueue_flux_pro_11_text_to_image_webhook<U: IntoUrl>(
args: FluxPro11Args<'_, U>
) -> Result<WebhookResponse, FalErrorPlus> {
let num_images = match args.num_images {
FluxPro11NumImages::One => 1,
FluxPro11NumImages::Two => 2,
FluxPro11NumImages::Three => 3,
FluxPro11NumImages::Four => 4,
};
let image_size = match args.aspect_ratio {
FluxPro11AspectRatio::Square => ImageSizeProperty::Square,
FluxPro11AspectRatio::SquareHd => ImageSizeProperty::SquareHd,
FluxPro11AspectRatio::LandscapeFourByThree => ImageSizeProperty::Landscape43,
FluxPro11AspectRatio::LandscapeSixteenByNine => ImageSizeProperty::Landscape169,
FluxPro11AspectRatio::PortraitThreeByFour => ImageSizeProperty::Portrait43,
FluxPro11AspectRatio::PortraitNineBySixteen => ImageSizeProperty::Portrait169,
};
let request = FluxProPlusTextToImageInput {
prompt: args.prompt.to_string(),
num_images: Some(num_images),
image_size: Some(image_size),
// Maybe expose
seed: None,
// Maybe abstract
enable_safety_checker: Some(false),
safety_tolerance: Some("5".to_string()), // 1 is most strict, 5 is most permissive
// Constants
output_format: Some("png".to_string()),
sync_mode: None, // Synchronous / slow
};
let result = v1_1(request)
.with_api_key(&args.api_key.0)
.queue_webhook(args.webhook_url)
.await;
result.map_err(|err| classify_fal_error(err))
}
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::image::enqueue_flux_pro_11_text_to_image_webhook::{enqueue_flux_pro_11_text_to_image_webhook, FluxPro11Args, FluxPro11AspectRatio, FluxPro11NumImages};
use errors::AnyhowResult;
use std::fs::read_to_string;
#[tokio::test]
#[ignore]
async fn test_flux_11_pro() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = FluxPro11Args {
prompt: "a giant red panda fighting a dragon in a futuristic city",
api_key: &api_key,
webhook_url: "https://example.com/webhook",
num_images: FluxPro11NumImages::One,
aspect_ratio: FluxPro11AspectRatio::LandscapeSixteenByNine,
};
let result = enqueue_flux_pro_11_text_to_image_webhook(args).await?;
Ok(())
}
}
@@ -0,0 +1,74 @@
use crate::creds::fal_api_key::FalApiKey;
use crate::error::classify_fal_error::classify_fal_error;
use crate::error::fal_error_plus::FalErrorPlus;
use fal::endpoints::fal_ai::flux_pro::v1_1_ultra::{v1_1_ultra, AspectRatioProperty, FluxProUltraTextToImageInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct FluxPro11UltraArgs<'a, U: IntoUrl> {
pub prompt: &'a str,
pub webhook_url: U,
pub api_key: &'a FalApiKey,
pub aspect_ratio: FluxPro11UltraAspectRatio,
pub num_images: FluxPro11UltraNumImages,
}
#[derive(Copy, Clone, Debug)]
pub enum FluxPro11UltraAspectRatio {
Square, // 1:1
LandscapeFourByThree, // 4:3
LandscapeSixteenByNine, // 16:9
PortraitThreeByFour, // 3:4
PortraitNineBySixteen, // 9:16
//Custom { width: u32, height: u32 }, // TODO
}
#[derive(Copy, Clone, Debug)]
pub enum FluxPro11UltraNumImages {
One, // Default
Two,
Three,
Four,
}
pub async fn enqueue_flux_pro_11_ultra_text_to_image_webhook<U: IntoUrl>(
args: FluxPro11UltraArgs<'_, U>
) -> Result<WebhookResponse, FalErrorPlus> {
let num_images = match args.num_images {
FluxPro11UltraNumImages::One => 1,
FluxPro11UltraNumImages::Two => 2,
FluxPro11UltraNumImages::Three => 3,
FluxPro11UltraNumImages::Four => 4,
};
let aspect_ratio = match args.aspect_ratio {
FluxPro11UltraAspectRatio::Square => AspectRatioProperty::Property_1_1,
FluxPro11UltraAspectRatio::LandscapeFourByThree => AspectRatioProperty::Property_4_3,
FluxPro11UltraAspectRatio::LandscapeSixteenByNine => AspectRatioProperty::Property_16_9,
FluxPro11UltraAspectRatio::PortraitThreeByFour => AspectRatioProperty::Property_3_4,
FluxPro11UltraAspectRatio::PortraitNineBySixteen => AspectRatioProperty::Property_9_16,
};
let request = FluxProUltraTextToImageInput {
prompt: args.prompt.to_string(),
num_images: Some(num_images),
aspect_ratio: Some(aspect_ratio),
// Maybe expose
seed: None,
raw: Some(true), // Generate less processed, more natural-looking images. Default is false.
// Maybe abstract
enable_safety_checker: Some(false),
safety_tolerance: Some("5".to_string()), // 1 is most strict, 5 is most permissive
// Constants
output_format: Some("png".to_string()),
sync_mode: None, // Synchronous / slow
};
let result = v1_1_ultra(request)
.with_api_key(&args.api_key.0)
.queue_webhook(args.webhook_url)
.await;
result.map_err(|err| classify_fal_error(err))
}
@@ -1 +1,5 @@
pub mod enqueue_flux_1_dev_text_to_image_webhook;
pub mod enqueue_flux_1_schnell_text_to_image_webhook;
pub mod enqueue_flux_pro_11_text_to_image_webhook;
pub mod enqueue_flux_pro_11_ultra_text_to_image_webhook;
pub mod remove_background_rembg_webhook;
@@ -0,0 +1,65 @@
use crate::creds::fal_api_key::FalApiKey;
use crate::error::classify_fal_error::classify_fal_error;
use crate::error::fal_error_plus::FalErrorPlus;
use fal::prelude::fal_ai::hunyuan3d::v21::{v21, Hunyuan3DInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct Hunyuan3d21Args<'a, U: IntoUrl, V: IntoUrl> {
pub image_url: U,
pub webhook_url: V,
pub api_key: &'a FalApiKey,
}
pub async fn enqueue_hunyuan_3d_2_1_image_to_3d_webhook<U: IntoUrl, V: IntoUrl>(
args: Hunyuan3d21Args<'_, U, V>
) -> Result<WebhookResponse, FalErrorPlus> {
let image_url = args.image_url.as_str().to_string();
let request = Hunyuan3DInput {
input_image_url: image_url,
textured_mesh: Some(true),
// TODO: Maybe expose these later
guidance_scale: None,
num_inference_steps: None,
octree_resolution: None,
seed: None,
};
let result = v21(request)
.with_api_key(&args.api_key.0)
.queue_webhook(args.webhook_url)
.await;
result.map_err(|err| classify_fal_error(err))
}
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::object::enqueue_hunyuan_3d_21_image_to_3d_webhook::{enqueue_hunyuan_3d_2_1_image_to_3d_webhook, Hunyuan3d21Args};
use errors::AnyhowResult;
use std::fs::read_to_string;
#[tokio::test]
#[ignore]
async fn test_hunyuan3d_21() -> AnyhowResult<()> {
let image_url = "https://cdn-2.fakeyou.com/media/p/a/c/7/j/pac7jgp2tkehm7j7sm4sky1fpnkrnbve/image_pac7jgp2tkehm7j7sm4sky1fpnkrnbve.png";
// XXX: Don't commit secrets!
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = Hunyuan3d21Args {
image_url: image_url,
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_hunyuan_3d_2_1_image_to_3d_webhook(args).await?;
Ok(())
}
}
@@ -5,21 +5,14 @@ use fal::endpoints::fal_ai::hunyuan3d::v2::{v2, Hunyuan3DInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct Hunyuan2Args<'a, U: IntoUrl, V: IntoUrl> {
pub struct Hunyuan3d2Args<'a, U: IntoUrl, V: IntoUrl> {
pub image_url: U,
pub webhook_url: V,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
pub enum Kling16Duration {
Default,
FiveSeconds,
TenSeconds,
}
pub async fn enqueue_hunyuan2_image_to_3d_webhook<U: IntoUrl, V: IntoUrl>(
args: Hunyuan2Args<'_, U, V>
pub async fn enqueue_hunyuan_3d_2_image_to_3d_webhook<U: IntoUrl, V: IntoUrl>(
args: Hunyuan3d2Args<'_, U, V>
) -> Result<WebhookResponse, FalErrorPlus> {
let image_url = args.image_url.as_str().to_string();
@@ -1 +1,2 @@
pub mod enqueue_hunyuan2_image_to_3d_webhook;
pub mod enqueue_hunyuan_3d_2_image_to_3d_webhook;
pub mod enqueue_hunyuan_3d_21_image_to_3d_webhook;
@@ -5,12 +5,13 @@ use fal::endpoints::fal_ai::kling_video::v1_6::pro::image_to_video::{image_to_vi
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct Kling16Args<'a, U: IntoUrl, V: IntoUrl> {
pub struct Kling16ProArgs<'a, U: IntoUrl, V: IntoUrl> {
pub image_url: U,
pub webhook_url: V,
pub prompt: &'a str,
pub api_key: &'a FalApiKey,
pub duration: Kling16Duration,
pub aspect_ratio: Kling16ProAspectRatio,
}
#[derive(Copy, Clone, Debug)]
@@ -20,8 +21,15 @@ pub enum Kling16Duration {
TenSeconds,
}
pub async fn enqueue_kling_16_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
args: Kling16Args<'_, U, V>
#[derive(Copy, Clone, Debug)]
pub enum Kling16ProAspectRatio {
Square, // 1:1
WideSixteenNine, // 16:9
TallNineSixteen, // 9:16
}
pub async fn enqueue_kling_16_pro_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
args: Kling16ProArgs<'_, U, V>
) -> Result<WebhookResponse, FalErrorPlus> {
let duration = match args.duration {
Kling16Duration::Default => None,
@@ -29,14 +37,21 @@ pub async fn enqueue_kling_16_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
Kling16Duration::TenSeconds => Some("10".to_string()),
};
let aspect_ratio = match args.aspect_ratio {
Kling16ProAspectRatio::Square => Some("1:1".to_string()),
Kling16ProAspectRatio::WideSixteenNine => Some("16:9".to_string()),
Kling16ProAspectRatio::TallNineSixteen => Some("9:16".to_string()),
};
let image_url = args.image_url.as_str().to_string();
let request = ProImageToVideoRequest {
image_url,
prompt: args.prompt.to_string(),
aspect_ratio: None,
cfg_scale: None,
aspect_ratio,
duration,
// Maybe expose these later
cfg_scale: None,
negative_prompt: None,
tail_image_url: None,
};
@@ -0,0 +1,98 @@
use crate::creds::fal_api_key::FalApiKey;
use crate::error::classify_fal_error::classify_fal_error;
use crate::error::fal_error_plus::FalErrorPlus;
use fal::endpoints::fal_ai::kling_video::v2_1::master::image_to_video::{image_to_video, MasterImageToVideoRequest};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct Kling21MasterArgs<'a, U: IntoUrl, V: IntoUrl> {
pub image_url: U,
pub webhook_url: V,
pub prompt: &'a str,
pub api_key: &'a FalApiKey,
pub duration: Kling21MasterDuration,
pub aspect_ratio: Kling21MasterAspectRatio,
}
#[derive(Copy, Clone, Debug)]
pub enum Kling21MasterDuration {
Default,
FiveSeconds,
TenSeconds,
}
#[derive(Copy, Clone, Debug)]
pub enum Kling21MasterAspectRatio {
Square, // 1:1
WideSixteenNine, // 16:9
TallNineSixteen, // 9:16
}
pub async fn enqueue_kling_21_master_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
args: Kling21MasterArgs<'_, U, V>
) -> Result<WebhookResponse, FalErrorPlus> {
let duration = match args.duration {
Kling21MasterDuration::Default => None,
Kling21MasterDuration::FiveSeconds => Some("5".to_string()), // Gross...
Kling21MasterDuration::TenSeconds => Some("10".to_string()),
};
let aspect_ratio = match args.aspect_ratio {
Kling21MasterAspectRatio::Square => Some("1:1".to_string()),
Kling21MasterAspectRatio::WideSixteenNine => Some("16:9".to_string()),
Kling21MasterAspectRatio::TallNineSixteen => Some("9:16".to_string()),
};
let image_url = args.image_url.as_str().to_string();
let request = MasterImageToVideoRequest {
image_url,
prompt: args.prompt.to_string(),
aspect_ratio,
duration,
// Maybe expose these later
cfg_scale: None,
negative_prompt: None,
tail_image_url: None,
};
let result = image_to_video(request)
.with_api_key(&args.api_key.0)
.queue_webhook(args.webhook_url)
.await;
result.map_err(|err| classify_fal_error(err))
}
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::video::enqueue_kling_21_master_image_to_video_webhook::{enqueue_kling_21_master_image_to_video_webhook, Kling21MasterArgs, Kling21MasterAspectRatio, Kling21MasterDuration};
use errors::AnyhowResult;
use std::fs::read_to_string;
#[tokio::test]
#[ignore]
async fn test_kling21_master_video() -> AnyhowResult<()> {
let image_url = "https://cdn-2.fakeyou.com/media/3/4/h/f/s/34hfsmt8e38rvne6mwa4pwbxr6292sgy/image_34hfsmt8e38rvne6mwa4pwbxr6292sgy.png";
// XXX: Don't commit secrets!
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = Kling21MasterArgs {
image_url: image_url,
prompt: "a shot of the mountains, the camera rotates around to show the mountain range, the sun begins to set, and the moon becomes visible",
api_key: &api_key,
duration: Kling21MasterDuration::Default,
aspect_ratio: Kling21MasterAspectRatio::WideSixteenNine,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_kling_21_master_image_to_video_webhook(args).await?;
Ok(())
}
}
@@ -0,0 +1,98 @@
use crate::creds::fal_api_key::FalApiKey;
use crate::error::classify_fal_error::classify_fal_error;
use crate::error::fal_error_plus::FalErrorPlus;
use fal::endpoints::fal_ai::kling_video::v2_1::pro::image_to_video::{image_to_video, ProImageToVideoRequest};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct Kling21ProArgs<'a, U: IntoUrl, V: IntoUrl> {
pub image_url: U,
pub webhook_url: V,
pub prompt: &'a str,
pub api_key: &'a FalApiKey,
pub duration: Kling21ProDuration,
pub aspect_ratio: Kling21ProAspectRatio,
}
#[derive(Copy, Clone, Debug)]
pub enum Kling21ProDuration {
Default,
FiveSeconds,
TenSeconds,
}
#[derive(Copy, Clone, Debug)]
pub enum Kling21ProAspectRatio {
Square, // 1:1
WideSixteenNine, // 16:9
TallNineSixteen, // 9:16
}
pub async fn enqueue_kling_21_pro_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
args: Kling21ProArgs<'_, U, V>
) -> Result<WebhookResponse, FalErrorPlus> {
let duration = match args.duration {
Kling21ProDuration::Default => None,
Kling21ProDuration::FiveSeconds => Some("5".to_string()), // Gross...
Kling21ProDuration::TenSeconds => Some("10".to_string()),
};
let aspect_ratio = match args.aspect_ratio {
Kling21ProAspectRatio::Square => Some("1:1".to_string()),
Kling21ProAspectRatio::WideSixteenNine => Some("16:9".to_string()),
Kling21ProAspectRatio::TallNineSixteen => Some("9:16".to_string()),
};
let image_url = args.image_url.as_str().to_string();
let request = ProImageToVideoRequest {
image_url,
prompt: args.prompt.to_string(),
aspect_ratio,
duration,
// Maybe expose these later
cfg_scale: None,
negative_prompt: None,
tail_image_url: None,
};
let result = image_to_video(request)
.with_api_key(&args.api_key.0)
.queue_webhook(args.webhook_url)
.await;
result.map_err(|err| classify_fal_error(err))
}
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::video::enqueue_kling_21_pro_image_to_video_webhook::{enqueue_kling_21_pro_image_to_video_webhook, Kling21ProArgs, Kling21ProAspectRatio, Kling21ProDuration};
use errors::AnyhowResult;
use std::fs::read_to_string;
#[tokio::test]
#[ignore]
async fn test_kling21_pro_video() -> AnyhowResult<()> {
let image_url = "https://cdn-2.fakeyou.com/media/3/4/h/f/s/34hfsmt8e38rvne6mwa4pwbxr6292sgy/image_34hfsmt8e38rvne6mwa4pwbxr6292sgy.png";
// XXX: Don't commit secrets!
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = Kling21ProArgs {
image_url: image_url,
prompt: "a shot of the mountains, the camera rotates around to show the mountain range",
api_key: &api_key,
duration: Kling21ProDuration::Default,
aspect_ratio: Kling21ProAspectRatio::WideSixteenNine,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_kling_21_pro_image_to_video_webhook(args).await?;
Ok(())
}
}
@@ -0,0 +1,98 @@
use crate::creds::fal_api_key::FalApiKey;
use crate::error::classify_fal_error::classify_fal_error;
use crate::error::fal_error_plus::FalErrorPlus;
use fal::endpoints::fal_ai::bytedance::seedance::v1::lite::image_to_video::image_to_video;
use fal::endpoints::fal_ai::bytedance::seedance::v1::lite::image_to_video::ImageToVideoRequest;
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct Seedance1LiteArgs<'a, U: IntoUrl, V: IntoUrl> {
pub image_url: U,
pub webhook_url: V,
pub prompt: &'a str,
pub api_key: &'a FalApiKey,
pub camera_fixed: bool,
pub duration: Seedance1LiteDuration,
pub resolution: Seedance1LiteResolution,
pub seed: Option<u32>,
}
#[derive(Copy, Clone, Debug)]
pub enum Seedance1LiteDuration {
FiveSeconds,
TenSeconds,
}
#[derive(Copy, Clone, Debug)]
pub enum Seedance1LiteResolution {
FourEightyP, // 480p
SevenTwentyP, // 720p
}
pub async fn enqueue_seedance_1_lite_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
args: Seedance1LiteArgs<'_, U, V>
) -> Result<WebhookResponse, FalErrorPlus> {
let duration = match args.duration {
Seedance1LiteDuration::FiveSeconds => Some("5".to_string()),
Seedance1LiteDuration::TenSeconds => Some("10".to_string()),
};
let resolution = match args.resolution {
Seedance1LiteResolution::FourEightyP => Some("480p".to_string()),
Seedance1LiteResolution::SevenTwentyP => Some("720p".to_string()),
};
let image_url = args.image_url.as_str().to_string();
let request = ImageToVideoRequest {
image_url,
prompt: args.prompt.to_string(),
duration,
resolution,
// TODO: Add these later
camera_fixed: None,
seed: -1,
};
let result = image_to_video(request)
.with_api_key(&args.api_key.0)
.queue_webhook(args.webhook_url)
.await;
result.map_err(|err| classify_fal_error(err))
}
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::video::enqueue_seedance_1_lite_image_to_video_webhook::{enqueue_seedance_1_lite_image_to_video_webhook, Seedance1LiteArgs, Seedance1LiteDuration, Seedance1LiteResolution};
use errors::AnyhowResult;
use std::fs::read_to_string;
#[tokio::test]
#[ignore]
async fn test_kling21_pro_video() -> AnyhowResult<()> {
let image_url = "https://cdn-2.fakeyou.com/media/3/4/h/f/s/34hfsmt8e38rvne6mwa4pwbxr6292sgy/image_34hfsmt8e38rvne6mwa4pwbxr6292sgy.png";
// XXX: Don't commit secrets!
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = Seedance1LiteArgs {
image_url: image_url,
prompt: "a shot of the mountains as the sun sets and reveals the moon and stars",
api_key: &api_key,
camera_fixed: false,
duration: Seedance1LiteDuration::FiveSeconds,
resolution: Seedance1LiteResolution::SevenTwentyP,
seed: None,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_seedance_1_lite_image_to_video_webhook(args).await?;
Ok(())
}
}
@@ -0,0 +1,102 @@
use crate::creds::fal_api_key::FalApiKey;
use crate::error::classify_fal_error::classify_fal_error;
use crate::error::fal_error_plus::FalErrorPlus;
use fal::endpoints::fal_ai::veo2::image_to_video::image_to_video;
use fal::endpoints::fal_ai::veo2::image_to_video::ImageToVideoInput;
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct Veo2Args<'a, U: IntoUrl, V: IntoUrl> {
pub image_url: U,
pub webhook_url: V,
pub prompt: &'a str,
pub api_key: &'a FalApiKey,
pub duration: Veo2Duration,
pub aspect_ratio: Veo2AspectRatio,
}
#[derive(Copy, Clone, Debug)]
pub enum Veo2Duration {
Default,
FiveSeconds,
SixSeconds,
SevenSeconds,
EightSeconds,
}
#[derive(Copy, Clone, Debug)]
pub enum Veo2AspectRatio {
Auto,
AutoPreferPortrait,
WideSixteenNine, // 16:9
TallNineSixteen, // 9:16
}
pub async fn enqueue_veo_2_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
args: Veo2Args<'_, U, V>
) -> Result<WebhookResponse, FalErrorPlus> {
let duration = match args.duration {
Veo2Duration::Default => None,
Veo2Duration::FiveSeconds => Some("5".to_string()),
Veo2Duration::SixSeconds => Some("6".to_string()),
Veo2Duration::SevenSeconds => Some("7".to_string()),
Veo2Duration::EightSeconds => Some("8".to_string()),
};
let aspect_ratio = match args.aspect_ratio {
Veo2AspectRatio::Auto => Some("auto".to_string()),
Veo2AspectRatio::AutoPreferPortrait => Some("auto_prefer_portrait".to_string()),
Veo2AspectRatio::WideSixteenNine => Some("16:9".to_string()),
Veo2AspectRatio::TallNineSixteen => Some("9:16".to_string()),
};
let image_url = args.image_url.as_str().to_string();
let request = ImageToVideoInput {
image_url,
prompt: args.prompt.to_string(),
aspect_ratio,
duration,
// Maybe expose these later
};
let result = image_to_video(request)
.with_api_key(&args.api_key.0)
.queue_webhook(args.webhook_url)
.await;
result.map_err(|err| classify_fal_error(err))
}
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::video::enqueue_veo_2_image_to_video_webhook::{enqueue_veo_2_image_to_video_webhook, Veo2Args, Veo2AspectRatio, Veo2Duration};
use errors::AnyhowResult;
use std::fs::read_to_string;
#[tokio::test]
#[ignore]
async fn test_kling21_pro_video() -> AnyhowResult<()> {
let image_url = "https://cdn-2.fakeyou.com/media/3/4/h/f/s/34hfsmt8e38rvne6mwa4pwbxr6292sgy/image_34hfsmt8e38rvne6mwa4pwbxr6292sgy.png";
// XXX: Don't commit secrets!
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = Veo2Args {
image_url: image_url,
prompt: "a shot of the mountains as the sun sets and reveals the moon and stars",
api_key: &api_key,
duration: Veo2Duration::Default,
aspect_ratio: Veo2AspectRatio::WideSixteenNine,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_veo_2_image_to_video_webhook(args).await?;
Ok(())
}
}
@@ -1 +1,5 @@
pub mod enqueue_kling_16_image_to_video_webhook;
pub mod enqueue_kling_16_pro_image_to_video_webhook;
pub mod enqueue_kling_21_master_image_to_video_webhook;
pub mod enqueue_kling_21_pro_image_to_video_webhook;
pub mod enqueue_seedance_1_lite_image_to_video_webhook;
pub mod enqueue_veo_2_image_to_video_webhook;
@@ -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 }
@@ -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<io::Error> for ClientError {
ClientError::IoError(error)
}
}
impl From<serde_json::Error> for ClientError {
fn from(error: serde_json::Error) -> Self {
ClientError::SerializationError(error)
}
}
impl From<reqwest::Error> for ClientError {
fn from(error: reqwest::Error) -> Self {
ClientError::ReqwestError(error)
}
}
@@ -0,0 +1,68 @@
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::image::generate_flux_pro_11_ultra_text_to_image::{GenerateFluxPro11UltraTextToImageRequest, GenerateFluxPro11UltraTextToImageResponse, GENERATE_FLUX_PRO_11_ULTRA_TEXT_TO_IMAGE_PATH};
use log::{debug, info};
use reqwest::Client;
pub async fn generate_flux_pro_11_ultra_text_to_image(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: GenerateFluxPro11UltraTextToImageRequest,
) -> Result<GenerateFluxPro11UltraTextToImageResponse, 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_FLUX_PRO_11_ULTRA_TEXT_TO_IMAGE_PATH)
}
@@ -0,0 +1 @@
pub mod generate_flux_pro_11_ultra_text_to_image;
@@ -0,0 +1,3 @@
pub mod image;
pub mod object;
pub mod video;
@@ -0,0 +1,68 @@
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;
pub async fn generate_hunyuan2_image_to_3d(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: GenerateHunyuan2ImageTo3dRequest,
) -> Result<GenerateHunyuan2ImageTo3dResponse, 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_IMAGE_TO_3D_URL_PATH)
}
@@ -0,0 +1 @@
pub mod generate_hunyuan2_image_to_3d;
@@ -0,0 +1,68 @@
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::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)
}
@@ -0,0 +1 @@
pub mod generate_kling_16_pro_image_to_video;
@@ -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;
@@ -1,4 +1,5 @@
use crate::error::api_error::ApiError;
use crate::error::storyteller_error::StorytellerError;
use crate::shared_response_types::media_file_cover_image_details::MediaFileCoverImageDetails;
use crate::shared_response_types::media_links::MediaLinks;
use crate::shared_response_types::simple_entity_stats::SimpleEntityStats;
@@ -23,16 +24,18 @@ use tokens::tokens::model_weights::ModelWeightToken;
use tokens::tokens::prompts::PromptToken;
/// Get details about a media file from our backend
pub async fn get_media_file(api_host: &ApiHost, media_file_token: &MediaFileToken) -> Result<GetMediaFileSuccessResponse, ApiError> {
pub async fn get_media_file(api_host: &ApiHost, media_file_token: &MediaFileToken) -> Result<GetMediaFileSuccessResponse, StorytellerError> {
let url = get_media_file_token_route(api_host, media_file_token);
debug!("Requesting {:?}", &url);
let response = http_get_anonymous(url).await?;
let response = filter_bad_response(response).await?;
let response_body = &response.text().await?;
let response_body = &response.text().await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
let media_file = serde_json::from_str(&response_body)?;
let media_file = serde_json::from_str(&response_body)
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
Ok(media_file)
}
@@ -1,5 +1,7 @@
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::shared_response_types::media_file_cover_image_details::MediaFileCoverImageDetails;
use crate::shared_response_types::media_links::MediaLinks;
use crate::shared_response_types::simple_entity_stats::SimpleEntityStats;
@@ -36,7 +38,7 @@ pub async fn upload_image_media_file_from_file<P: AsRef<Path>>(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
path: P,
) -> Result<UploadImageMediaFileSuccessResponse, ApiError> {
) -> Result<UploadImageMediaFileSuccessResponse, StorytellerError> {
let url = get_route(api_host);
@@ -46,12 +48,14 @@ pub async fn upload_image_media_file_from_file<P: AsRef<Path>>(
let client = Client::builder()
.gzip(true)
.build()?;
.build()
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
let form = Form::new()
.text("uuid_idempotency_token", generate_random_uuid())
.file("file", path)
.await?;
.await
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
let mut request_builder = client.post(url)
.header("User-Agent", USER_AGENT)
@@ -66,12 +70,15 @@ pub async fn upload_image_media_file_from_file<P: AsRef<Path>>(
let response = request_builder
.multipart(form)
.send()
.await?;
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
let response = filter_bad_response(response).await?;
let response_body = &response.text().await?;
let response_body = &response.text().await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
let media_file = serde_json::from_str(&response_body)?;
let media_file = serde_json::from_str(&response_body)
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
Ok(media_file)
}
@@ -54,7 +54,7 @@ pub async fn upload_media_file_from_file<P: AsRef<Path>>(
success: result.success,
media_file_token: result.media_file_token,
}),
Err(err) => Err(StorytellerError::Api(err)),
Err(err) => Err(err),
}
}
Some(_) => Err(StorytellerError::Client(ClientError::FileTypeNotHandled(path.as_ref().to_path_buf()))),
+25
View File
@@ -0,0 +1,25 @@
[package]
name = "web_base64"
edition = "2024"
version = "0.0.1"
publish = false
[package.metadata.cargo-udeps.ignore]
normal = ["workspace-build-acceleration"]
[lib]
name = "web_base64"
path = "src/lib.rs"
[dependencies]
# Workspace hack (faster compile times)
workspace-build-acceleration = { workspace = true }
# Internal
# (None)
# External
base64 = { workspace = true }
[dev-dependencies]
# (None)
+5
View File
@@ -0,0 +1,5 @@
//! Base64 encoding and decoding library that handles data from the web.
//! Most of the base64 we receive from the web is of a particular encoding
//! type, and we also have to juggle URL prefixes.
pub mod web_base64_decode;
@@ -0,0 +1,14 @@
use base64::prelude::BASE64_STANDARD;
use base64::Engine;
pub fn web_base64_decode(base64_string: &str) -> Result<Vec<u8>, base64::DecodeError> {
// Remove the data URL prefix if it exists
// eg: "data:image/png;base64,..."
let base64_data = if base64_string.starts_with("data:") {
base64_string.split(',').nth(1).unwrap_or(base64_string)
} else {
base64_string
};
BASE64_STANDARD.decode(base64_data)
}
@@ -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<u32>,
pub maybe_frame_height: Option<u32>,
pub checksum_sha2: &'a str,
// Media info for certain product areas
pub maybe_engine_category: Option<MediaFileEngineCategory>, // 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,
@@ -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,
@@ -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<u32>,
checksum_sha2: Option<String>, // NB: Non-nullable field
// Media info for certain product areas
pub maybe_engine_category: Option<MediaFileEngineCategory>, // 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<MediaFileEngineCategory>) -> 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
@@ -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)
@@ -0,0 +1,49 @@
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use utoipa::ToSchema;
pub const GENERATE_FLUX_1_DEV_TEXT_TO_IMAGE_PATH: &str = "/v1/generate/image/flux_1_dev_text_to_image";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GenerateFlux1DevTextToImageRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// Text prompt to generate the image from.
pub prompt: Option<String>,
/// Aspect ratio of the output images.
pub aspect_ratio: Option<GenerateFlux1DevTextToImageAspectRatio>,
/// Number of images to generate. Default is one.
pub num_images: Option<GenerateFlux1DevTextToImageNumImages>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateFlux1DevTextToImageAspectRatio {
Square, // 1:1
SquareHd, // 1:1
LandscapeFourByThree, // 4:3
LandscapeSixteenByNine, // 16:9
PortraitThreeByFour, // 3:4
PortraitNineBySixteen, // 9:16
//Custom { width: u32, height: u32 }, // TODO
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateFlux1DevTextToImageNumImages {
One, // Default
Two,
Three,
Four,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GenerateFlux1DevTextToImageResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -0,0 +1,49 @@
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use utoipa::ToSchema;
pub const GENERATE_FLUX_1_SCHNELL_TEXT_TO_IMAGE_PATH: &str = "/v1/generate/image/flux_1_schnell_text_to_image";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GenerateFlux1SchnellTextToImageRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// Text prompt to generate the image from.
pub prompt: Option<String>,
/// Aspect ratio of the output images.
pub aspect_ratio: Option<GenerateFlux1SchnellTextToImageAspectRatio>,
/// Number of images to generate. Default is one.
pub num_images: Option<GenerateFlux1SchnellTextToImageNumImages>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateFlux1SchnellTextToImageAspectRatio {
Square, // 1:1
SquareHd, // 1:1
LandscapeFourByThree, // 4:3
LandscapeSixteenByNine, // 16:9
PortraitThreeByFour, // 3:4
PortraitNineBySixteen, // 9:16
//Custom { width: u32, height: u32 }, // TODO
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateFlux1SchnellTextToImageNumImages {
One, // Default
Two,
Three,
Four,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GenerateFlux1SchnellTextToImageResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -0,0 +1,46 @@
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use utoipa::ToSchema;
pub const GENERATE_FLUX_PRO_11_TEXT_TO_IMAGE_PATH: &str = "/v1/generate/image/flux_pro_1.1_text_to_image";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GenerateFluxPro11TextToImageRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// Text prompt to generate the image from.
pub prompt: Option<String>,
/// Aspect ratio of the output images.
pub aspect_ratio: Option<GenerateFluxPro11TextToImageAspectRatio>,
/// Number of images to generate. Default is one.
pub num_images: Option<GenerateFluxPro11TextToImageNumImages>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateFluxPro11TextToImageAspectRatio {
Square, // 1:1
LandscapeFourByThree, // 4:3
LandscapeSixteenByNine, // 16:9
PortraitThreeByFour, // 3:4
PortraitNineBySixteen, // 9:16
//Custom { width: u32, height: u32 }, // TODO
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateFluxPro11TextToImageNumImages {
One, // Default
Two,
Three,
Four,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GenerateFluxPro11TextToImageResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -0,0 +1,46 @@
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use utoipa::ToSchema;
pub const GENERATE_FLUX_PRO_11_ULTRA_TEXT_TO_IMAGE_PATH: &str = "/v1/generate/image/flux_pro_1.1_ultra_text_to_image";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GenerateFluxPro11UltraTextToImageRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// Text prompt to generate the image from.
pub prompt: Option<String>,
/// Aspect ratio of the output images.
pub aspect_ratio: Option<GenerateFluxPro11UltraTextToImageAspectRatio>,
/// Number of images to generate. Default is one.
pub num_images: Option<GenerateFluxPro11UltraTextToImageNumImages>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateFluxPro11UltraTextToImageAspectRatio {
Square, // 1:1
LandscapeFourByThree, // 4:3
LandscapeSixteenByNine, // 16:9
PortraitThreeByFour, // 3:4
PortraitNineBySixteen, // 9:16
//Custom { width: u32, height: u32 }, // TODO
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateFluxPro11UltraTextToImageNumImages {
One, // Default
Two,
Three,
Four,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GenerateFluxPro11UltraTextToImageResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -1 +1,5 @@
pub mod generate_flux_1_dev_text_to_image;
pub mod generate_flux_1_schnell_text_to_image;
pub mod generate_flux_pro_11_text_to_image;
pub mod generate_flux_pro_11_ultra_text_to_image;
pub mod remove_image_background;
@@ -1,25 +1,22 @@
use serde::{Deserialize, Serialize};
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
const GENERATE_KLING_1_6_IMAGE_TO_VIDEO_URL_PATH: &str = "/v1/generate/video/kling_16_image_to_video";
pub const GENERATE_HUNYUAN_2_1_IMAGE_TO_3D_URL_PATH: &str = "/v1/generate/object/hunyuan_2.1_image_to_3d";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GenerateKling16ImageToVideoRequest {
pub struct GenerateHunyuan21ImageTo3dRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// Source of the image file to convert to video.
/// Source of the file to convert to 3D
/// It must be an image.
pub media_file_token: Option<MediaFileToken>,
/// Optional text prompt.
pub prompt: Option<String>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GenerateKling16ImageToVideoResponse {
pub struct GenerateHunyuan21ImageTo3dResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -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<MediaFileToken>,
/// Optional text prompt.
pub prompt: Option<String>,
}
#[derive(Serialize, Deserialize, ToSchema)]
@@ -1 +1,2 @@
pub mod generate_hunyuan_21_image_to_3d;
pub mod generate_hunyuan_2_image_to_3d;
@@ -0,0 +1,49 @@
use serde::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
pub const GENERATE_KLING_1_6_PRO_IMAGE_TO_VIDEO_URL_PATH: &str = "/v1/generate/video/kling_1.6_pro_image_to_video";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GenerateKling16ProImageToVideoRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// Source of the image file to convert to video.
/// It must be an image.
pub media_file_token: Option<MediaFileToken>,
/// Optional text prompt.
pub prompt: Option<String>,
/// Optional: aspect ratio of the generated video.
pub aspect_ratio: Option<GenerateKling16ProAspectRatio>,
/// Optional: aspect ratio of the generated video.
pub duration: Option<GenerateKling16ProDuration>,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateKling16ProAspectRatio {
/// 16:9 aspect ratio
WideSixteenNine,
/// 9:16 aspect ratio
TallNineSixteen,
/// 1:1 aspect ratio
Square,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateKling16ProDuration {
FiveSeconds,
TenSeconds,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GenerateKling16ProImageToVideoResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -0,0 +1,49 @@
use serde::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
pub const GENERATE_KLING_2_1_MASTER_IMAGE_TO_VIDEO_URL_PATH: &str = "/v1/generate/video/kling_2.1_master_image_to_video";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GenerateKling21MasterImageToVideoRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// Source of the image file to convert to video.
/// It must be an image.
pub media_file_token: Option<MediaFileToken>,
/// Optional text prompt.
pub prompt: Option<String>,
/// Optional: aspect ratio of the generated video.
pub aspect_ratio: Option<GenerateKling21MasterAspectRatio>,
/// Optional: aspect ratio of the generated video.
pub duration: Option<GenerateKling21MasterDuration>,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateKling21MasterAspectRatio {
/// 16:9 aspect ratio
WideSixteenNine,
/// 9:16 aspect ratio
TallNineSixteen,
/// 1:1 aspect ratio
Square,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateKling21MasterDuration {
FiveSeconds,
TenSeconds,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GenerateKling21MasterImageToVideoResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -0,0 +1,49 @@
use serde::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
pub const GENERATE_KLING_2_1_PRO_IMAGE_TO_VIDEO_URL_PATH: &str = "/v1/generate/video/kling_2.1_pro_image_to_video";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GenerateKling21ProImageToVideoRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// Source of the image file to convert to video.
/// It must be an image.
pub media_file_token: Option<MediaFileToken>,
/// Optional text prompt.
pub prompt: Option<String>,
/// Optional: aspect ratio of the generated video.
pub aspect_ratio: Option<GenerateKling21ProAspectRatio>,
/// Optional: aspect ratio of the generated video.
pub duration: Option<GenerateKling21ProDuration>,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateKling21ProAspectRatio {
/// 16:9 aspect ratio
WideSixteenNine,
/// 9:16 aspect ratio
TallNineSixteen,
/// 1:1 aspect ratio
Square,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateKling21ProDuration {
FiveSeconds,
TenSeconds,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GenerateKling21ProImageToVideoResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -0,0 +1,45 @@
use serde::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
pub const GENERATE_SEEDANCE_1_0_LITE_IMAGE_TO_VIDEO_URL_PATH: &str = "/v1/generate/video/seedance_1.0_lite_image_to_video";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GenerateSeedance10LiteImageToVideoRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// Source of the image file to convert to video.
/// It must be an image.
pub media_file_token: Option<MediaFileToken>,
/// Optional text prompt.
pub prompt: Option<String>,
/// Optional: aspect ratio of the generated video.
pub resolution: Option<GenerateSeedance10LiteResolution>,
/// Optional: aspect ratio of the generated video.
pub duration: Option<GenerateSeedance10LiteDuration>,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateSeedance10LiteResolution {
FourEightyP,
SevenTwentyP,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateSeedance10LiteDuration {
FiveSeconds,
TenSeconds,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GenerateSeedance10LiteImageToVideoResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -0,0 +1,49 @@
use serde::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
pub const GENERATE_VEO_2_IMAGE_TO_VIDEO_URL_PATH: &str = "/v1/generate/video/veo_2_image_to_video";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GenerateVeo2ImageToVideoRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// Source of the image file to convert to video.
/// It must be an image.
pub media_file_token: Option<MediaFileToken>,
/// Optional text prompt.
pub prompt: Option<String>,
/// Optional: aspect ratio of the generated video.
pub aspect_ratio: Option<GenerateVeo2AspectRatio>,
/// Optional: aspect ratio of the generated video.
pub duration: Option<GenerateVeo2Duration>,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateVeo2AspectRatio {
Auto,
AutoPreferPortrait,
WideSixteenNine, // 16:9
TallNineSixteen, // 9:16
}
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateVeo2Duration {
FiveSeconds,
SixSeconds,
SevenSeconds,
EightSeconds,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GenerateVeo2ImageToVideoResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -1 +1,5 @@
pub mod generate_kling_1_6_image_to_video;
pub mod generate_kling_1_6_pro_image_to_video;
pub mod generate_kling_2_1_master_image_to_video;
pub mod generate_kling_2_1_pro_image_to_video;
pub mod generate_seedance_1_0_lite_image_to_video;
pub mod generate_veo_2_image_to_video;
@@ -1,5 +1,47 @@
use utoipa::OpenApi;
use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageAspectRatio;
use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageNumImages;
use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageRequest;
use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageResponse;
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageAspectRatio;
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageNumImages;
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageRequest;
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageResponse;
use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageAspectRatio;
use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageNumImages;
use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageRequest;
use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageResponse;
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageAspectRatio;
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageNumImages;
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageRequest;
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageResponse;
use artcraft_api_defs::generate::image::remove_image_background::RemoveImageBackgroundRequest;
use artcraft_api_defs::generate::image::remove_image_background::RemoveImageBackgroundResponse;
use artcraft_api_defs::generate::object::generate_hunyuan_21_image_to_3d::GenerateHunyuan21ImageTo3dRequest;
use artcraft_api_defs::generate::object::generate_hunyuan_21_image_to_3d::GenerateHunyuan21ImageTo3dResponse;
use artcraft_api_defs::generate::object::generate_hunyuan_2_image_to_3d::GenerateHunyuan2ImageTo3dRequest;
use artcraft_api_defs::generate::object::generate_hunyuan_2_image_to_3d::GenerateHunyuan2ImageTo3dResponse;
use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProAspectRatio;
use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProDuration;
use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProImageToVideoRequest;
use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProImageToVideoResponse;
use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::GenerateKling21MasterAspectRatio;
use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::GenerateKling21MasterDuration;
use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::GenerateKling21MasterImageToVideoRequest;
use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::GenerateKling21MasterImageToVideoResponse;
use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::GenerateKling21ProAspectRatio;
use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::GenerateKling21ProDuration;
use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::GenerateKling21ProImageToVideoRequest;
use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::GenerateKling21ProImageToVideoResponse;
use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteDuration;
use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteImageToVideoRequest;
use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteImageToVideoResponse;
use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteResolution;
use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2AspectRatio;
use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2Duration;
use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2ImageToVideoRequest;
use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2ImageToVideoResponse;
use billing_component::stripe::http_endpoints::checkout::create::stripe_create_checkout_session_error::CreateCheckoutSessionError;
use billing_component::stripe::http_endpoints::checkout::create::stripe_create_checkout_session_json_handler::*;
use billing_component::users::http_endpoints::list_active_user_subscriptions_handler::*;
@@ -187,6 +229,18 @@ use crate::http_server::web_utils::response_success_helpers::*;
crate::http_server::endpoints::featured_items::create_featured_item_handler::create_featured_item_handler,
crate::http_server::endpoints::featured_items::delete_featured_item_handler::delete_featured_item_handler,
crate::http_server::endpoints::featured_items::get_is_featured_item_handler::get_is_featured_item_handler,
crate::http_server::endpoints::generate::image::generate_flux_1_dev_text_to_image_handler::generate_flux_1_dev_text_to_image_handler,
crate::http_server::endpoints::generate::image::generate_flux_1_schnell_text_to_image_handler::generate_flux_1_schnell_text_to_image_handler,
crate::http_server::endpoints::generate::image::generate_flux_pro_11_text_to_image_handler::generate_flux_pro_11_text_to_image_handler,
crate::http_server::endpoints::generate::image::generate_flux_pro_11_ultra_text_to_image_handler::generate_flux_pro_11_ultra_text_to_image_handler,
crate::http_server::endpoints::generate::image::remove_image_background_handler::remove_image_background_handler,
crate::http_server::endpoints::generate::object::generate_hunyuan_21_image_to_3d_handler::generate_hunyuan_21_image_to_3d_handler,
crate::http_server::endpoints::generate::object::generate_hunyuan_2_image_to_3d_handler::generate_hunyuan_2_image_to_3d_handler,
crate::http_server::endpoints::generate::video::generate_kling_1_6_pro_video_handler::generate_kling_1_6_pro_video_handler,
crate::http_server::endpoints::generate::video::generate_kling_2_1_master_video_handler::generate_kling_2_1_master_video_handler,
crate::http_server::endpoints::generate::video::generate_kling_2_1_pro_video_handler::generate_kling_2_1_pro_video_handler,
crate::http_server::endpoints::generate::video::generate_seedance_1_0_lite_image_to_video_handler::generate_seedance_1_0_lite_image_to_video_handler,
crate::http_server::endpoints::generate::video::generate_veo_2_image_to_video_handler::generate_veo_2_image_to_video_handler,
crate::http_server::endpoints::image_studio::prompt::enqueue_studio_image_generation_handler::enqueue_studio_image_generation_handler,
crate::http_server::endpoints::image_studio::upload::upload_snapshot_media_file_handler::upload_snapshot_media_file_handler,
crate::http_server::endpoints::inference_job::delete::dismiss_finished_session_jobs_handler::dismiss_finished_session_jobs_handler,
@@ -340,9 +394,6 @@ use crate::http_server::web_utils::response_success_helpers::*;
AppStateServerInfo,
AppStateStatusAlertCategory,
AppStateStatusAlertInfo,
EnqueueStudioImageGenRequest,
EnqueueImageGenRequestSuccessResponse,
EnqueueImageGenRequestError,
AppStateSubscriptionProductKey,
AppStateUserInfo,
AppStateUserLocale,
@@ -354,8 +405,6 @@ use crate::http_server::web_utils::response_success_helpers::*;
BatchGetMediaFilesQueryParams,
BatchGetMediaFilesSuccessResponse,
BatchGetUserBookmarksError,
UploadSnapshotMediaFileForm,
UploadSnapshotMediaFileSuccessResponse,
BatchGetUserBookmarksQueryParams,
BatchGetUserBookmarksResponse,
BatchGetUserRatingError,
@@ -429,9 +478,6 @@ use crate::http_server::web_utils::response_success_helpers::*;
EditUsernameResponse,
EnqueueFaceFusionCropDimensions,
EnqueueFaceFusionWorkflowError,
EnqueueStudioGen2Request,
EnqueueStudioGen2Response,
EnqueueStudioGen2Error,
EnqueueFaceFusionWorkflowRequest,
EnqueueFaceFusionWorkflowSuccessResponse,
EnqueueFbxToGltfRequest,
@@ -440,10 +486,16 @@ use crate::http_server::web_utils::response_success_helpers::*;
EnqueueGptSovitsModelDownloadError,
EnqueueGptSovitsModelDownloadRequest,
EnqueueGptSovitsModelDownloadSuccessResponse,
EnqueueImageGenRequestError,
EnqueueImageGenRequestSuccessResponse,
EnqueueLivePortraitCropDimensions,
EnqueueLivePortraitWorkflowError,
EnqueueLivePortraitWorkflowRequest,
EnqueueLivePortraitWorkflowSuccessResponse,
EnqueueStudioGen2Error,
EnqueueStudioGen2Request,
EnqueueStudioGen2Response,
EnqueueStudioImageGenRequest,
EnqueueTTSRequest,
EnqueueTTSRequestError,
EnqueueTTSRequestSuccessResponse,
@@ -457,6 +509,46 @@ use crate::http_server::web_utils::response_success_helpers::*;
FeaturedMediaFile,
FeaturedModelWeightForList,
FundamentalFrequencyMethod,
GenerateFlux1DevTextToImageAspectRatio,
GenerateFlux1DevTextToImageNumImages,
GenerateFlux1DevTextToImageRequest,
GenerateFlux1DevTextToImageResponse,
GenerateFlux1SchnellTextToImageAspectRatio,
GenerateFlux1SchnellTextToImageNumImages,
GenerateFlux1SchnellTextToImageRequest,
GenerateFlux1SchnellTextToImageResponse,
GenerateFluxPro11TextToImageAspectRatio,
GenerateFluxPro11TextToImageNumImages,
GenerateFluxPro11TextToImageRequest,
GenerateFluxPro11TextToImageResponse,
GenerateFluxPro11UltraTextToImageAspectRatio,
GenerateFluxPro11UltraTextToImageNumImages,
GenerateFluxPro11UltraTextToImageRequest,
GenerateFluxPro11UltraTextToImageResponse,
GenerateHunyuan21ImageTo3dRequest,
GenerateHunyuan21ImageTo3dResponse,
GenerateHunyuan2ImageTo3dRequest,
GenerateHunyuan2ImageTo3dResponse,
GenerateKling16ProAspectRatio,
GenerateKling16ProDuration,
GenerateKling16ProImageToVideoRequest,
GenerateKling16ProImageToVideoResponse,
GenerateKling21MasterAspectRatio,
GenerateKling21MasterDuration,
GenerateKling21MasterImageToVideoRequest,
GenerateKling21MasterImageToVideoResponse,
GenerateKling21ProAspectRatio,
GenerateKling21ProDuration,
GenerateKling21ProImageToVideoRequest,
GenerateKling21ProImageToVideoResponse,
GenerateSeedance10LiteDuration,
GenerateSeedance10LiteImageToVideoRequest,
GenerateSeedance10LiteImageToVideoResponse,
GenerateSeedance10LiteResolution,
GenerateVeo2AspectRatio,
GenerateVeo2Duration,
GenerateVeo2ImageToVideoRequest,
GenerateVeo2ImageToVideoResponse,
GetInferenceJobStatusError,
GetInferenceJobStatusPathInfo,
GetInferenceJobStatusSuccessResponse,
@@ -569,6 +661,8 @@ use crate::http_server::web_utils::response_success_helpers::*;
RedeemBetaKeyError,
RedeemBetaKeyRequest,
RedeemBetaKeySuccessResponse,
RemoveImageBackgroundRequest,
RemoveImageBackgroundResponse,
RenameMediaFileError,
RenameMediaFileRequest,
RequestDetailsResponse,
@@ -636,6 +730,8 @@ use crate::http_server::web_utils::response_success_helpers::*;
UploadSavedSceneMediaFileSuccessResponse,
UploadSceneSnapshotMediaFileForm,
UploadSceneSnapshotMediaFileSuccessResponse,
UploadSnapshotMediaFileForm,
UploadSnapshotMediaFileSuccessResponse,
UploadStudioShotFileForm,
UploadStudioShotSuccessResponse,
UploadVideoMediaSuccessResponse,
@@ -0,0 +1,90 @@
use actix_http::StatusCode;
use actix_web::{HttpResponse, HttpResponseBuilder, ResponseError};
use std::error::Error;
use std::fmt::{Display, Formatter};
use utoipa::ToSchema;
#[derive(Debug)]
pub enum CommonWebError {
// TODO: Bad input with error fields vector.
/// Bad input with a user-facing error message.
BadInputWithSimpleMessage(String),
/// Entity not found.
NotFound,
/// Not authorized to perform the action.
NotAuthorized,
/// Server error with no reason.
ServerError,
}
impl Error for CommonWebError {}
impl Display for CommonWebError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
#[derive(Debug, Serialize)]
struct SerializeSimpleErrorWithoutMessage<'a> {
success: bool,
error_code: u16,
error_code_str: Option<&'a str>,
}
#[derive(Debug, Serialize)]
struct SerializeSimpleErrorWithMessage<'a> {
success: bool,
error_code: u16,
error_code_str: Option<&'a str>,
message: &'a str,
}
impl ResponseError for CommonWebError {
fn status_code(&self) -> StatusCode {
match self {
CommonWebError::BadInputWithSimpleMessage(_) => StatusCode::BAD_REQUEST,
CommonWebError::NotFound => StatusCode::NOT_FOUND,
CommonWebError::NotAuthorized => StatusCode::UNAUTHORIZED,
CommonWebError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
}
}
fn error_response(&self) -> HttpResponse {
match self {
CommonWebError::BadInputWithSimpleMessage(msg) => {
HttpResponse::BadRequest()
.json(SerializeSimpleErrorWithMessage {
success: false,
error_code: self.status_code().as_u16(),
error_code_str: self.status_code().canonical_reason(),
message: msg,
})
},
_ => {
HttpResponseBuilder::new(self.status_code())
.json(SerializeSimpleErrorWithoutMessage {
success: false,
error_code: self.status_code().as_u16(),
error_code_str: self.status_code().canonical_reason(),
})
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use actix_http::body::MessageBody;
#[test]
fn test_bad_input_json() {
let error = CommonWebError::BadInputWithSimpleMessage("foo bar baz".to_string());
let response = error.error_response();
let bytes = response.into_body().try_into_bytes().unwrap();
let body = String::from_utf8(bytes.to_vec()).unwrap();
assert_eq!(body, "{\"success\":false,\"error_code\":400,\"error_code_str\":\"Bad Request\",\"message\":\"foo bar baz\"}");
}
}
@@ -1,3 +1,4 @@
pub mod common_web_error;
pub mod media;
pub mod media_file_origin_details;
pub mod media_file_social_meta_lite;
@@ -0,0 +1,158 @@
use std::fmt;
use std::sync::Arc;
use crate::http_server::common_requests::media_file_token_path_info::MediaFileTokenPathInfo;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::common_responses::media::media_links::MediaLinks;
use crate::http_server::deprecated_endpoints::engine::create_scene_handler::CreateSceneError;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::http_server::web_utils::response_success_helpers::simple_json_success;
use crate::state::server_state::ServerState;
use crate::util::delete_role_disambiguation::{delete_role_disambiguation, DeleteRole};
use actix_web::error::ResponseError;
use actix_web::http::StatusCode;
use actix_web::web::Json;
use actix_web::web::Path;
use actix_web::{web, HttpRequest, HttpResponse};
use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::{GenerateFlux1DevTextToImageAspectRatio, GenerateFlux1DevTextToImageNumImages, GenerateFlux1DevTextToImageRequest, GenerateFlux1DevTextToImageResponse};
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use enums::common::visibility::Visibility;
use fal_client::requests::webhook::image::enqueue_flux_1_dev_text_to_image_webhook::enqueue_flux_1_dev_text_to_image_webhook;
use fal_client::requests::webhook::image::enqueue_flux_1_dev_text_to_image_webhook::{Flux1DevArgs, Flux1DevAspectRatio, Flux1DevNumImages};
use http_server_common::request::get_request_ip::get_request_ip;
use http_server_common::response::serialize_as_json_error::serialize_as_json_error;
use idempotency::uuid::generate_random_uuid;
use log::{error, info, warn};
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::insert_generic_inference_job_for_fal_queue;
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory;
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::InsertGenericInferenceForFalArgs;
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
use mysql_queries::queries::media_files::get::get_media_file::{get_media_file, MediaFile};
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
/// Flux 1 Dev text to image
#[utoipa::path(
post,
tag = "Generate Images",
path = "/v1/generate/image/flux_1_dev_text_to_image",
responses(
(status = 200, description = "Success", body = GenerateFlux1DevTextToImageResponse),
),
params(
("request" = GenerateFlux1DevTextToImageRequest, description = "Payload for Request"),
)
)]
pub async fn generate_flux_1_dev_text_to_image_handler(
http_request: HttpRequest,
request: Json<GenerateFlux1DevTextToImageRequest>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<GenerateFlux1DevTextToImageResponse>, CommonWebError> {
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
CommonWebError::ServerError
})?;
let maybe_avt_token = server_state
.avt_cookie_manager
.get_avt_token_from_request(&http_request);
// TODO: Limit usage for new accounts. Billing, free credits metering, etc.
//let user_session = match maybe_user_session {
// Some(session) => session,
// None => {
// warn!("not logged in");
// return Err(CommonWebError::NotAuthorized);
// }
//};
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
}
insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool)
.await
.map_err(|err| {
error!("Error inserting idempotency token: {:?}", err);
CommonWebError::BadInputWithSimpleMessage("repeated idempotency token".to_string())
})?;
const IS_MOD : bool = false;
info!("Fal webhook URL: {}", server_state.fal.webhook_url);
let aspect_ratio = match request.aspect_ratio {
Some(GenerateFlux1DevTextToImageAspectRatio::Square) => Flux1DevAspectRatio::Square,
Some(GenerateFlux1DevTextToImageAspectRatio::SquareHd) => Flux1DevAspectRatio::SquareHd,
Some(GenerateFlux1DevTextToImageAspectRatio::LandscapeFourByThree) => Flux1DevAspectRatio::LandscapeFourByThree,
Some(GenerateFlux1DevTextToImageAspectRatio::LandscapeSixteenByNine) => Flux1DevAspectRatio::LandscapeSixteenByNine,
Some(GenerateFlux1DevTextToImageAspectRatio::PortraitThreeByFour) => Flux1DevAspectRatio::PortraitThreeByFour,
Some(GenerateFlux1DevTextToImageAspectRatio::PortraitNineBySixteen) => Flux1DevAspectRatio::PortraitNineBySixteen,
None => Flux1DevAspectRatio::LandscapeSixteenByNine, // Default
};
let num_images = match request.num_images {
Some(GenerateFlux1DevTextToImageNumImages::One) => Flux1DevNumImages::One,
Some(GenerateFlux1DevTextToImageNumImages::Two) => Flux1DevNumImages::Two,
Some(GenerateFlux1DevTextToImageNumImages::Three) => Flux1DevNumImages::Three,
Some(GenerateFlux1DevTextToImageNumImages::Four) => Flux1DevNumImages::Four,
None => Flux1DevNumImages::One, // Default
};
let args = Flux1DevArgs {
prompt: request.prompt.as_deref().unwrap_or(""),
webhook_url: &server_state.fal.webhook_url,
api_key: &server_state.fal.api_key,
aspect_ratio,
num_images,
};
let fal_result = enqueue_flux_1_dev_text_to_image_webhook(args)
.await
.map_err(|err| {
warn!("Error calling enqueue_flux_1_dev_text_to_image_webhook: {:?}", err);
CommonWebError::ServerError
})?;
let external_job_id = fal_result.request_id
.ok_or_else(|| {
warn!("Fal request_id is None");
CommonWebError::ServerError
})?;
info!("Fal request_id: {}", external_job_id);
let ip_address = get_request_ip(&http_request);
let db_result = insert_generic_inference_job_for_fal_queue(InsertGenericInferenceForFalArgs {
uuid_idempotency_token: &request.uuid_idempotency_token,
maybe_external_third_party_id: &external_job_id,
fal_category: FalCategory::ImageGeneration,
maybe_inference_args: None,
maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token),
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_pool: &server_state.mysql_pool,
}).await;
let job_token = match db_result {
Ok(token) => token,
Err(err) => {
warn!("Error inserting generic inference job for FAL queue: {:?}", err);
return Err(CommonWebError::ServerError);
}
};
Ok(Json(GenerateFlux1DevTextToImageResponse {
success: true,
inference_job_token: job_token,
}))
}
@@ -0,0 +1,158 @@
use std::fmt;
use std::sync::Arc;
use crate::http_server::common_requests::media_file_token_path_info::MediaFileTokenPathInfo;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::common_responses::media::media_links::MediaLinks;
use crate::http_server::deprecated_endpoints::engine::create_scene_handler::CreateSceneError;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::http_server::web_utils::response_success_helpers::simple_json_success;
use crate::state::server_state::ServerState;
use crate::util::delete_role_disambiguation::{delete_role_disambiguation, DeleteRole};
use actix_web::error::ResponseError;
use actix_web::http::StatusCode;
use actix_web::web::Json;
use actix_web::web::Path;
use actix_web::{web, HttpRequest, HttpResponse};
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::{GenerateFlux1SchnellTextToImageAspectRatio, GenerateFlux1SchnellTextToImageNumImages, GenerateFlux1SchnellTextToImageRequest, GenerateFlux1SchnellTextToImageResponse};
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use enums::common::visibility::Visibility;
use fal_client::requests::webhook::image::enqueue_flux_1_schnell_text_to_image_webhook::enqueue_flux_1_schnell_text_to_image_webhook;
use fal_client::requests::webhook::image::enqueue_flux_1_schnell_text_to_image_webhook::{Flux1SchnellArgs, Flux1SchnellAspectRatio, Flux1SchnellNumImages};
use http_server_common::request::get_request_ip::get_request_ip;
use http_server_common::response::serialize_as_json_error::serialize_as_json_error;
use idempotency::uuid::generate_random_uuid;
use log::{error, info, warn};
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::insert_generic_inference_job_for_fal_queue;
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory;
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::InsertGenericInferenceForFalArgs;
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
use mysql_queries::queries::media_files::get::get_media_file::{get_media_file, MediaFile};
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
/// Flux 1 Schnell text to image
#[utoipa::path(
post,
tag = "Generate Images",
path = "/v1/generate/image/flux_1_schnell_text_to_image",
responses(
(status = 200, description = "Success", body = GenerateFlux1SchnellTextToImageResponse),
),
params(
("request" = GenerateFlux1SchnellTextToImageRequest, description = "Payload for Request"),
)
)]
pub async fn generate_flux_1_schnell_text_to_image_handler(
http_request: HttpRequest,
request: Json<GenerateFlux1SchnellTextToImageRequest>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<GenerateFlux1SchnellTextToImageResponse>, CommonWebError> {
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
CommonWebError::ServerError
})?;
let maybe_avt_token = server_state
.avt_cookie_manager
.get_avt_token_from_request(&http_request);
// TODO: Limit usage for new accounts. Billing, free credits metering, etc.
//let user_session = match maybe_user_session {
// Some(session) => session,
// None => {
// warn!("not logged in");
// return Err(CommonWebError::NotAuthorized);
// }
//};
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
}
insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool)
.await
.map_err(|err| {
error!("Error inserting idempotency token: {:?}", err);
CommonWebError::BadInputWithSimpleMessage("repeated idempotency token".to_string())
})?;
const IS_MOD : bool = false;
info!("Fal webhook URL: {}", server_state.fal.webhook_url);
let aspect_ratio = match request.aspect_ratio {
Some(GenerateFlux1SchnellTextToImageAspectRatio::Square) => Flux1SchnellAspectRatio::Square,
Some(GenerateFlux1SchnellTextToImageAspectRatio::SquareHd) => Flux1SchnellAspectRatio::SquareHd,
Some(GenerateFlux1SchnellTextToImageAspectRatio::LandscapeFourByThree) => Flux1SchnellAspectRatio::LandscapeFourByThree,
Some(GenerateFlux1SchnellTextToImageAspectRatio::LandscapeSixteenByNine) => Flux1SchnellAspectRatio::LandscapeSixteenByNine,
Some(GenerateFlux1SchnellTextToImageAspectRatio::PortraitThreeByFour) => Flux1SchnellAspectRatio::PortraitThreeByFour,
Some(GenerateFlux1SchnellTextToImageAspectRatio::PortraitNineBySixteen) => Flux1SchnellAspectRatio::PortraitNineBySixteen,
None => Flux1SchnellAspectRatio::LandscapeSixteenByNine, // Default
};
let num_images = match request.num_images {
Some(GenerateFlux1SchnellTextToImageNumImages::One) => Flux1SchnellNumImages::One,
Some(GenerateFlux1SchnellTextToImageNumImages::Two) => Flux1SchnellNumImages::Two,
Some(GenerateFlux1SchnellTextToImageNumImages::Three) => Flux1SchnellNumImages::Three,
Some(GenerateFlux1SchnellTextToImageNumImages::Four) => Flux1SchnellNumImages::Four,
None => Flux1SchnellNumImages::One, // Default
};
let args = Flux1SchnellArgs {
prompt: request.prompt.as_deref().unwrap_or(""),
webhook_url: &server_state.fal.webhook_url,
api_key: &server_state.fal.api_key,
aspect_ratio,
num_images,
};
let fal_result = enqueue_flux_1_schnell_text_to_image_webhook(args)
.await
.map_err(|err| {
warn!("Error calling enqueue_flux_1_schnell_text_to_image_webhook: {:?}", err);
CommonWebError::ServerError
})?;
let external_job_id = fal_result.request_id
.ok_or_else(|| {
warn!("Fal request_id is None");
CommonWebError::ServerError
})?;
info!("Fal request_id: {}", external_job_id);
let ip_address = get_request_ip(&http_request);
let db_result = insert_generic_inference_job_for_fal_queue(InsertGenericInferenceForFalArgs {
uuid_idempotency_token: &request.uuid_idempotency_token,
maybe_external_third_party_id: &external_job_id,
fal_category: FalCategory::ImageGeneration,
maybe_inference_args: None,
maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token),
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_pool: &server_state.mysql_pool,
}).await;
let job_token = match db_result {
Ok(token) => token,
Err(err) => {
warn!("Error inserting generic inference job for FAL queue: {:?}", err);
return Err(CommonWebError::ServerError);
}
};
Ok(Json(GenerateFlux1SchnellTextToImageResponse {
success: true,
inference_job_token: job_token,
}))
}
@@ -0,0 +1,159 @@
use std::fmt;
use std::sync::Arc;
use crate::http_server::common_requests::media_file_token_path_info::MediaFileTokenPathInfo;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::common_responses::media::media_links::MediaLinks;
use crate::http_server::deprecated_endpoints::engine::create_scene_handler::CreateSceneError;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::http_server::web_utils::response_success_helpers::simple_json_success;
use crate::state::server_state::ServerState;
use crate::util::delete_role_disambiguation::{delete_role_disambiguation, DeleteRole};
use actix_web::error::ResponseError;
use actix_web::http::StatusCode;
use actix_web::web::Json;
use actix_web::web::Path;
use actix_web::{web, HttpRequest, HttpResponse};
use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageResponse;
use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::{GenerateFluxPro11TextToImageAspectRatio, GenerateFluxPro11TextToImageNumImages, GenerateFluxPro11TextToImageRequest};
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use enums::common::visibility::Visibility;
use fal_client::requests::webhook::image::enqueue_flux_pro_11_text_to_image_webhook::FluxPro11Args;
use fal_client::requests::webhook::image::enqueue_flux_pro_11_text_to_image_webhook::{enqueue_flux_pro_11_text_to_image_webhook, FluxPro11AspectRatio, FluxPro11NumImages};
use http_server_common::request::get_request_ip::get_request_ip;
use http_server_common::response::serialize_as_json_error::serialize_as_json_error;
use idempotency::uuid::generate_random_uuid;
use log::{error, info, warn};
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::insert_generic_inference_job_for_fal_queue;
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory;
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::InsertGenericInferenceForFalArgs;
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
use mysql_queries::queries::media_files::get::get_media_file::{get_media_file, MediaFile};
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
/// Flux Pro 1.1
#[utoipa::path(
post,
tag = "Generate Images",
path = "/v1/generate/image/flux_pro_1.1_text_to_image",
responses(
(status = 200, description = "Success", body = GenerateFluxPro11TextToImageResponse),
),
params(
("request" = GenerateFluxPro11TextToImageRequest, description = "Payload for Request"),
)
)]
pub async fn generate_flux_pro_11_text_to_image_handler(
http_request: HttpRequest,
request: Json<GenerateFluxPro11TextToImageRequest>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<GenerateFluxPro11TextToImageResponse>, CommonWebError> {
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
CommonWebError::ServerError
})?;
let maybe_avt_token = server_state
.avt_cookie_manager
.get_avt_token_from_request(&http_request);
// TODO: Limit usage for new accounts. Billing, free credits metering, etc.
//let user_session = match maybe_user_session {
// Some(session) => session,
// None => {
// warn!("not logged in");
// return Err(GenerateFluxProUltraTextToImageError::NotAuthorized);
// }
//};
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
}
insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool)
.await
.map_err(|err| {
error!("Error inserting idempotency token: {:?}", err);
CommonWebError::BadInputWithSimpleMessage("invalid idempotency token".to_string())
})?;
const IS_MOD : bool = false;
info!("Fal webhook URL: {}", server_state.fal.webhook_url);
let aspect_ratio = match request.aspect_ratio {
Some(GenerateFluxPro11TextToImageAspectRatio::Square) => FluxPro11AspectRatio::Square,
Some(GenerateFluxPro11TextToImageAspectRatio::LandscapeFourByThree) => FluxPro11AspectRatio::LandscapeFourByThree,
Some(GenerateFluxPro11TextToImageAspectRatio::LandscapeSixteenByNine) => FluxPro11AspectRatio::LandscapeSixteenByNine,
Some(GenerateFluxPro11TextToImageAspectRatio::PortraitThreeByFour) => FluxPro11AspectRatio::PortraitThreeByFour,
Some(GenerateFluxPro11TextToImageAspectRatio::PortraitNineBySixteen) => FluxPro11AspectRatio::PortraitNineBySixteen,
None => FluxPro11AspectRatio::LandscapeSixteenByNine, // Default
};
let num_images = match request.num_images {
Some(GenerateFluxPro11TextToImageNumImages::One) => FluxPro11NumImages::One,
Some(GenerateFluxPro11TextToImageNumImages::Two) => FluxPro11NumImages::Two,
Some(GenerateFluxPro11TextToImageNumImages::Three) => FluxPro11NumImages::Three,
Some(GenerateFluxPro11TextToImageNumImages::Four) => FluxPro11NumImages::Four,
None => FluxPro11NumImages::One, // Default
};
let args = FluxPro11Args {
prompt: request.prompt.as_deref().unwrap_or(""),
webhook_url: &server_state.fal.webhook_url,
api_key: &server_state.fal.api_key,
aspect_ratio,
num_images,
};
let fal_result = enqueue_flux_pro_11_text_to_image_webhook(args)
.await
.map_err(|err| {
warn!("Error calling enqueue_flux_pro_text_to_image_webhook: {:?}", err);
CommonWebError::ServerError
})?;
let external_job_id = fal_result.request_id
.ok_or_else(|| {
warn!("Fal request_id is None");
CommonWebError::ServerError
})?;
info!("Fal request_id: {}", external_job_id);
let ip_address = get_request_ip(&http_request);
let db_result = insert_generic_inference_job_for_fal_queue(InsertGenericInferenceForFalArgs {
uuid_idempotency_token: &request.uuid_idempotency_token,
maybe_external_third_party_id: &external_job_id,
fal_category: FalCategory::ImageGeneration,
maybe_inference_args: None,
maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token),
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_pool: &server_state.mysql_pool,
}).await;
let job_token = match db_result {
Ok(token) => token,
Err(err) => {
warn!("Error inserting generic inference job for FAL queue: {:?}", err);
return Err(CommonWebError::ServerError);
}
};
Ok(Json(GenerateFluxPro11TextToImageResponse {
success: true,
inference_job_token: job_token,
}))
}
@@ -0,0 +1,159 @@
use std::fmt;
use std::sync::Arc;
use crate::http_server::common_requests::media_file_token_path_info::MediaFileTokenPathInfo;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::common_responses::media::media_links::MediaLinks;
use crate::http_server::deprecated_endpoints::engine::create_scene_handler::CreateSceneError;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::http_server::web_utils::response_success_helpers::simple_json_success;
use crate::state::server_state::ServerState;
use crate::util::delete_role_disambiguation::{delete_role_disambiguation, DeleteRole};
use actix_web::error::ResponseError;
use actix_web::http::StatusCode;
use actix_web::web::Json;
use actix_web::web::Path;
use actix_web::{web, HttpRequest, HttpResponse};
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageResponse;
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::{GenerateFluxPro11UltraTextToImageAspectRatio, GenerateFluxPro11UltraTextToImageNumImages, GenerateFluxPro11UltraTextToImageRequest};
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use enums::common::visibility::Visibility;
use fal_client::requests::webhook::image::enqueue_flux_pro_11_ultra_text_to_image_webhook::FluxPro11UltraArgs;
use fal_client::requests::webhook::image::enqueue_flux_pro_11_ultra_text_to_image_webhook::{enqueue_flux_pro_11_ultra_text_to_image_webhook, FluxPro11UltraAspectRatio, FluxPro11UltraNumImages};
use http_server_common::request::get_request_ip::get_request_ip;
use http_server_common::response::serialize_as_json_error::serialize_as_json_error;
use idempotency::uuid::generate_random_uuid;
use log::{error, info, warn};
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::insert_generic_inference_job_for_fal_queue;
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory;
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::InsertGenericInferenceForFalArgs;
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
use mysql_queries::queries::media_files::get::get_media_file::{get_media_file, MediaFile};
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
/// Flux Pro 1.1 Ultra
#[utoipa::path(
post,
tag = "Generate Images",
path = "/v1/generate/image/flux_pro_1.1_ultra_text_to_image",
responses(
(status = 200, description = "Success", body = GenerateFluxPro11UltraTextToImageResponse),
),
params(
("request" = GenerateFluxPro11UltraTextToImageRequest, description = "Payload for Request"),
)
)]
pub async fn generate_flux_pro_11_ultra_text_to_image_handler(
http_request: HttpRequest,
request: Json<GenerateFluxPro11UltraTextToImageRequest>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<GenerateFluxPro11UltraTextToImageResponse>, CommonWebError> {
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
CommonWebError::ServerError
})?;
let maybe_avt_token = server_state
.avt_cookie_manager
.get_avt_token_from_request(&http_request);
// TODO: Limit usage for new accounts. Billing, free credits metering, etc.
//let user_session = match maybe_user_session {
// Some(session) => session,
// None => {
// warn!("not logged in");
// return Err(GenerateFluxProUltraTextToImageError::NotAuthorized);
// }
//};
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
}
insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool)
.await
.map_err(|err| {
error!("Error inserting idempotency token: {:?}", err);
CommonWebError::BadInputWithSimpleMessage("invalid idempotency token".to_string())
})?;
const IS_MOD : bool = false;
info!("Fal webhook URL: {}", server_state.fal.webhook_url);
let aspect_ratio = match request.aspect_ratio {
Some(GenerateFluxPro11UltraTextToImageAspectRatio::Square) => FluxPro11UltraAspectRatio::Square,
Some(GenerateFluxPro11UltraTextToImageAspectRatio::LandscapeFourByThree) => FluxPro11UltraAspectRatio::LandscapeFourByThree,
Some(GenerateFluxPro11UltraTextToImageAspectRatio::LandscapeSixteenByNine) => FluxPro11UltraAspectRatio::LandscapeSixteenByNine,
Some(GenerateFluxPro11UltraTextToImageAspectRatio::PortraitThreeByFour) => FluxPro11UltraAspectRatio::PortraitThreeByFour,
Some(GenerateFluxPro11UltraTextToImageAspectRatio::PortraitNineBySixteen) => FluxPro11UltraAspectRatio::PortraitNineBySixteen,
None => FluxPro11UltraAspectRatio::LandscapeSixteenByNine, // Default
};
let num_images = match request.num_images {
Some(GenerateFluxPro11UltraTextToImageNumImages::One) => FluxPro11UltraNumImages::One,
Some(GenerateFluxPro11UltraTextToImageNumImages::Two) => FluxPro11UltraNumImages::Two,
Some(GenerateFluxPro11UltraTextToImageNumImages::Three) => FluxPro11UltraNumImages::Three,
Some(GenerateFluxPro11UltraTextToImageNumImages::Four) => FluxPro11UltraNumImages::Four,
None => FluxPro11UltraNumImages::One, // Default
};
let args = FluxPro11UltraArgs {
prompt: request.prompt.as_deref().unwrap_or(""),
webhook_url: &server_state.fal.webhook_url,
api_key: &server_state.fal.api_key,
aspect_ratio,
num_images,
};
let fal_result = enqueue_flux_pro_11_ultra_text_to_image_webhook(args)
.await
.map_err(|err| {
warn!("Error calling enqueue_flux_pro_ultra_text_to_image_webhook: {:?}", err);
CommonWebError::ServerError
})?;
let external_job_id = fal_result.request_id
.ok_or_else(|| {
warn!("Fal request_id is None");
CommonWebError::ServerError
})?;
info!("Fal request_id: {}", external_job_id);
let ip_address = get_request_ip(&http_request);
let db_result = insert_generic_inference_job_for_fal_queue(InsertGenericInferenceForFalArgs {
uuid_idempotency_token: &request.uuid_idempotency_token,
maybe_external_third_party_id: &external_job_id,
fal_category: FalCategory::ImageGeneration,
maybe_inference_args: None,
maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token),
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_pool: &server_state.mysql_pool,
}).await;
let job_token = match db_result {
Ok(token) => token,
Err(err) => {
warn!("Error inserting generic inference job for FAL queue: {:?}", err);
return Err(CommonWebError::ServerError);
}
};
Ok(Json(GenerateFluxPro11UltraTextToImageResponse {
success: true,
inference_job_token: job_token,
}))
}
@@ -1 +1,5 @@
pub mod generate_flux_1_dev_text_to_image_handler;
pub mod generate_flux_1_schnell_text_to_image_handler;
pub mod generate_flux_pro_11_text_to_image_handler;
pub mod generate_flux_pro_11_ultra_text_to_image_handler;
pub mod remove_image_background_handler;
@@ -2,9 +2,12 @@ use std::fmt;
use std::sync::Arc;
use crate::http_server::common_requests::media_file_token_path_info::MediaFileTokenPathInfo;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::common_responses::media::media_links::MediaLinks;
use crate::http_server::deprecated_endpoints::engine::create_scene_handler::CreateSceneError;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::http_server::web_utils::response_success_helpers::simple_json_success;
use crate::state::server_state::ServerState;
use crate::util::delete_role_disambiguation::{delete_role_disambiguation, DeleteRole};
@@ -29,42 +32,6 @@ use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_
use mysql_queries::queries::media_files::get::get_media_file::{get_media_file, MediaFile};
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
// =============== Error Response ===============
#[derive(Debug, Serialize, ToSchema)]
pub enum RemoveImageBackgroundError {
BadInput(String),
NotFound,
NotAuthorized,
ServerError,
}
impl ResponseError for RemoveImageBackgroundError {
fn status_code(&self) -> StatusCode {
match *self {
RemoveImageBackgroundError::BadInput(_) => StatusCode::BAD_REQUEST,
RemoveImageBackgroundError::NotFound => StatusCode::NOT_FOUND,
RemoveImageBackgroundError::NotAuthorized => StatusCode::UNAUTHORIZED,
RemoveImageBackgroundError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
}
}
fn error_response(&self) -> HttpResponse {
serialize_as_json_error(self)
}
}
// NB: Not using derive_more::Display since Clion doesn't understand it.
impl fmt::Display for RemoveImageBackgroundError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)
}
}
// =============== Handler ===============
/// Background removal
#[utoipa::path(
@@ -73,9 +40,6 @@ impl fmt::Display for RemoveImageBackgroundError {
path = "/v1/generate/image/remove_background",
responses(
(status = 200, description = "Success", body = RemoveImageBackgroundResponse),
(status = 400, description = "Bad input", body = RemoveImageBackgroundError),
(status = 401, description = "Not authorized", body = RemoveImageBackgroundError),
(status = 500, description = "Server error", body = RemoveImageBackgroundError),
),
params(
("request" = RemoveImageBackgroundRequest, description = "Payload for Request"),
@@ -85,14 +49,14 @@ pub async fn remove_image_background_handler(
http_request: HttpRequest,
request: Json<RemoveImageBackgroundRequest>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<RemoveImageBackgroundResponse>, RemoveImageBackgroundError> {
) -> Result<Json<RemoveImageBackgroundResponse>, CommonWebError> {
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
RemoveImageBackgroundError::ServerError
CommonWebError::ServerError
})?;
let maybe_avt_token = server_state
@@ -113,19 +77,19 @@ pub async fn remove_image_background_handler(
Some(token) => token,
None => {
warn!("No media file token provided");
return Err(RemoveImageBackgroundError::BadInput("No media file token provided".to_string()));
return Err(CommonWebError::BadInputWithSimpleMessage("No media file token provided".to_string()));
}
};
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
return Err(RemoveImageBackgroundError::BadInput(reason));
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
}
insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool)
.await
.map_err(|err| {
error!("Error inserting idempotency token: {:?}", err);
RemoveImageBackgroundError::BadInput("invalid idempotency token".to_string())
CommonWebError::BadInputWithSimpleMessage("invalid idempotency token".to_string())
})?;
const IS_MOD : bool = false;
@@ -139,16 +103,16 @@ pub async fn remove_image_background_handler(
Ok(Some(media_file)) => media_file,
Ok(None) => {
warn!("MediaFile not found: {:?}", media_file_token);
return Err(RemoveImageBackgroundError::NotFound);
return Err(CommonWebError::NotFound);
},
Err(err) => {
warn!("Error looking up media_file: {:?}", err);
return Err(RemoveImageBackgroundError::ServerError);
return Err(CommonWebError::ServerError);
}
};
if !media_file.media_type.is_jpg_or_png_or_legacy_image() {
return Err(RemoveImageBackgroundError::BadInput("Media file must be a JPG or PNG image".to_string()));
return Err(CommonWebError::BadInputWithSimpleMessage("Media file must be a JPG or PNG image".to_string()));
}
let media_domain = get_media_domain(&http_request);
@@ -175,13 +139,13 @@ pub async fn remove_image_background_handler(
.await
.map_err(|err| {
warn!("Error calling remove_background_rembg_webhook: {:?}", err);
RemoveImageBackgroundError::ServerError
CommonWebError::ServerError
})?;
let external_job_id = fal_result.request_id
.ok_or_else(|| {
warn!("Fal request_id is None");
RemoveImageBackgroundError::ServerError
CommonWebError::ServerError
})?;
info!("Fal request_id: {}", external_job_id);
@@ -204,7 +168,7 @@ pub async fn remove_image_background_handler(
Ok(token) => token,
Err(err) => {
warn!("Error inserting generic inference job for FAL queue: {:?}", err);
return Err(RemoveImageBackgroundError::ServerError);
return Err(CommonWebError::ServerError);
}
};
@@ -0,0 +1,180 @@
use std::fmt;
use std::sync::Arc;
use crate::http_server::common_requests::media_file_token_path_info::MediaFileTokenPathInfo;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::common_responses::media::media_links::MediaLinks;
use crate::http_server::deprecated_endpoints::engine::create_scene_handler::CreateSceneError;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::http_server::web_utils::response_success_helpers::simple_json_success;
use crate::state::server_state::ServerState;
use crate::util::delete_role_disambiguation::{delete_role_disambiguation, DeleteRole};
use actix_web::error::ResponseError;
use actix_web::http::StatusCode;
use actix_web::web::Json;
use actix_web::web::Path;
use actix_web::{web, HttpRequest, HttpResponse};
use artcraft_api_defs::generate::object::generate_hunyuan_21_image_to_3d::GenerateHunyuan21ImageTo3dRequest;
use artcraft_api_defs::generate::object::generate_hunyuan_21_image_to_3d::GenerateHunyuan21ImageTo3dResponse;
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use enums::common::visibility::Visibility;
use fal_client::requests::webhook::object::enqueue_hunyuan_3d_21_image_to_3d_webhook::enqueue_hunyuan_3d_2_1_image_to_3d_webhook;
use fal_client::requests::webhook::object::enqueue_hunyuan_3d_21_image_to_3d_webhook::Hunyuan3d21Args;
use http_server_common::request::get_request_ip::get_request_ip;
use http_server_common::response::serialize_as_json_error::serialize_as_json_error;
use idempotency::uuid::generate_random_uuid;
use log::{error, info, warn};
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::insert_generic_inference_job_for_fal_queue;
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory;
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::InsertGenericInferenceForFalArgs;
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
use mysql_queries::queries::media_files::get::get_media_file::{get_media_file, MediaFile};
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
/// Hunyuan 2.1 Image to 3D
#[utoipa::path(
post,
tag = "Generate Objects",
path = "/v1/generate/object/hunyuan_2.1_image_to_3d",
responses(
(status = 200, description = "Success", body = GenerateHunyuan21ImageTo3dResponse),
),
params(
("request" = GenerateHunyuan21ImageTo3dRequest, description = "Payload for Request"),
)
)]
pub async fn generate_hunyuan_21_image_to_3d_handler(
http_request: HttpRequest,
request: Json<GenerateHunyuan21ImageTo3dRequest>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<GenerateHunyuan21ImageTo3dResponse>, CommonWebError> {
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
CommonWebError::ServerError
})?;
let maybe_avt_token = server_state
.avt_cookie_manager
.get_avt_token_from_request(&http_request);
// TODO: Limit usage for new accounts. Billing, free credits metering, etc.
//let user_session = match maybe_user_session {
// Some(session) => session,
// None => {
// warn!("not logged in");
// return Err(CommonWebError::NotAuthorized);
// }
//};
let media_file_token = match &request.media_file_token {
Some(token) => token,
None => {
warn!("No media file token provided");
return Err(CommonWebError::BadInputWithSimpleMessage("No media file token provided".to_string()));
}
};
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
}
insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool)
.await
.map_err(|err| {
error!("Error inserting idempotency token: {:?}", err);
CommonWebError::BadInputWithSimpleMessage("invalid idempotency token".to_string())
})?;
const IS_MOD : bool = false;
let media_file_lookup_result = get_media_file(
media_file_token,
IS_MOD,
&server_state.mysql_pool,
).await;
let media_file = match media_file_lookup_result {
Ok(Some(media_file)) => media_file,
Ok(None) => {
warn!("MediaFile not found: {:?}", media_file_token);
return Err(CommonWebError::NotFound);
},
Err(err) => {
warn!("Error looking up media_file: {:?}", err);
return Err(CommonWebError::ServerError);
}
};
if !media_file.media_type.is_jpg_or_png_or_legacy_image() {
return Err(CommonWebError::BadInputWithSimpleMessage("Media file must be a JPG or PNG image".to_string()));
}
let media_domain = get_media_domain(&http_request);
let bucket_path = MediaFileBucketPath::from_object_hash(
&media_file.public_bucket_directory_hash,
media_file.maybe_public_bucket_prefix.as_deref(),
media_file.maybe_public_bucket_extension.as_deref());
let media_links = MediaLinks::from_media_path_and_env(
media_domain,
server_state.server_environment,
&bucket_path);
info!("Fal webhook URL: {}", server_state.fal.webhook_url);
let args = Hunyuan3d21Args {
image_url: media_links.cdn_url,
webhook_url: &server_state.fal.webhook_url,
api_key: &server_state.fal.api_key,
};
let fal_result = enqueue_hunyuan_3d_2_1_image_to_3d_webhook(args)
.await
.map_err(|err| {
warn!("Error calling enqueue_hunyuan_3d_2_1_image_to_3d_webhook: {:?}", err);
CommonWebError::ServerError
})?;
let external_job_id = fal_result.request_id
.ok_or_else(|| {
warn!("Fal request_id is None");
CommonWebError::ServerError
})?;
info!("Fal request_id: {}", external_job_id);
let ip_address = get_request_ip(&http_request);
let db_result = insert_generic_inference_job_for_fal_queue(InsertGenericInferenceForFalArgs {
uuid_idempotency_token: &request.uuid_idempotency_token,
maybe_external_third_party_id: &external_job_id,
fal_category: FalCategory::ObjectGeneration,
maybe_inference_args: None,
maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token),
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_pool: &server_state.mysql_pool,
}).await;
let job_token = match db_result {
Ok(token) => token,
Err(err) => {
warn!("Error inserting generic inference job for FAL queue: {:?}", err);
return Err(CommonWebError::ServerError);
}
};
Ok(Json(GenerateHunyuan21ImageTo3dResponse {
success: true,
inference_job_token: job_token,
}))
}
@@ -2,6 +2,7 @@ use std::fmt;
use std::sync::Arc;
use crate::http_server::common_requests::media_file_token_path_info::MediaFileTokenPathInfo;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::common_responses::media::media_links::MediaLinks;
use crate::http_server::deprecated_endpoints::engine::create_scene_handler::CreateSceneError;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
@@ -19,8 +20,8 @@ use artcraft_api_defs::generate::object::generate_hunyuan_2_image_to_3d::Generat
use artcraft_api_defs::generate::object::generate_hunyuan_2_image_to_3d::GenerateHunyuan2ImageTo3dResponse;
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use enums::common::visibility::Visibility;
use fal_client::requests::webhook::object::enqueue_hunyuan2_image_to_3d_webhook::enqueue_hunyuan2_image_to_3d_webhook;
use fal_client::requests::webhook::object::enqueue_hunyuan2_image_to_3d_webhook::Hunyuan2Args;
use fal_client::requests::webhook::object::enqueue_hunyuan_3d_2_image_to_3d_webhook::enqueue_hunyuan_3d_2_image_to_3d_webhook;
use fal_client::requests::webhook::object::enqueue_hunyuan_3d_2_image_to_3d_webhook::Hunyuan3d2Args;
use http_server_common::request::get_request_ip::get_request_ip;
use http_server_common::response::serialize_as_json_error::serialize_as_json_error;
use idempotency::uuid::generate_random_uuid;
@@ -33,51 +34,13 @@ use mysql_queries::queries::media_files::get::get_media_file::{get_media_file, M
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
// =============== Error Response ===============
#[derive(Debug, Serialize, ToSchema)]
pub enum GenerateHunyuan2ImageTo3dError {
BadInput(String),
NotFound,
NotAuthorized,
ServerError,
}
impl ResponseError for GenerateHunyuan2ImageTo3dError {
fn status_code(&self) -> StatusCode {
match *self {
GenerateHunyuan2ImageTo3dError::BadInput(_) => StatusCode::BAD_REQUEST,
GenerateHunyuan2ImageTo3dError::NotFound => StatusCode::NOT_FOUND,
GenerateHunyuan2ImageTo3dError::NotAuthorized => StatusCode::UNAUTHORIZED,
GenerateHunyuan2ImageTo3dError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
}
}
fn error_response(&self) -> HttpResponse {
serialize_as_json_error(self)
}
}
// NB: Not using derive_more::Display since Clion doesn't understand it.
impl fmt::Display for GenerateHunyuan2ImageTo3dError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)
}
}
// =============== Handler ===============
/// Background removal
/// Hunyuan 2.0 Image to 3D
#[utoipa::path(
post,
tag = "Generate Objects",
path = "/v1/generate/object/hunyuan_2_image_to_3d",
responses(
(status = 200, description = "Success", body = GenerateHunyuan2ImageTo3dResponse),
(status = 400, description = "Bad input", body = GenerateHunyuan2ImageTo3dError),
(status = 401, description = "Not authorized", body = GenerateHunyuan2ImageTo3dError),
(status = 500, description = "Server error", body = GenerateHunyuan2ImageTo3dError),
),
params(
("request" = GenerateHunyuan2ImageTo3dRequest, description = "Payload for Request"),
@@ -87,14 +50,14 @@ pub async fn generate_hunyuan_2_image_to_3d_handler(
http_request: HttpRequest,
request: Json<GenerateHunyuan2ImageTo3dRequest>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<GenerateHunyuan2ImageTo3dResponse>, GenerateHunyuan2ImageTo3dError> {
) -> Result<Json<GenerateHunyuan2ImageTo3dResponse>, CommonWebError> {
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
GenerateHunyuan2ImageTo3dError::ServerError
CommonWebError::ServerError
})?;
let maybe_avt_token = server_state
@@ -107,7 +70,7 @@ pub async fn generate_hunyuan_2_image_to_3d_handler(
// Some(session) => session,
// None => {
// warn!("not logged in");
// return Err(GenerateHunyuan2ImageTo3dError::NotAuthorized);
// return Err(CommonWebError::NotAuthorized);
// }
//};
@@ -115,19 +78,19 @@ pub async fn generate_hunyuan_2_image_to_3d_handler(
Some(token) => token,
None => {
warn!("No media file token provided");
return Err(GenerateHunyuan2ImageTo3dError::BadInput("No media file token provided".to_string()));
return Err(CommonWebError::BadInputWithSimpleMessage("No media file token provided".to_string()));
}
};
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
return Err(GenerateHunyuan2ImageTo3dError::BadInput(reason));
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
}
insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool)
.await
.map_err(|err| {
error!("Error inserting idempotency token: {:?}", err);
GenerateHunyuan2ImageTo3dError::BadInput("invalid idempotency token".to_string())
CommonWebError::BadInputWithSimpleMessage("invalid idempotency token".to_string())
})?;
const IS_MOD : bool = false;
@@ -141,16 +104,16 @@ pub async fn generate_hunyuan_2_image_to_3d_handler(
Ok(Some(media_file)) => media_file,
Ok(None) => {
warn!("MediaFile not found: {:?}", media_file_token);
return Err(GenerateHunyuan2ImageTo3dError::NotFound);
return Err(CommonWebError::NotFound);
},
Err(err) => {
warn!("Error looking up media_file: {:?}", err);
return Err(GenerateHunyuan2ImageTo3dError::ServerError);
return Err(CommonWebError::ServerError);
}
};
if !media_file.media_type.is_jpg_or_png_or_legacy_image() {
return Err(GenerateHunyuan2ImageTo3dError::BadInput("Media file must be a JPG or PNG image".to_string()));
return Err(CommonWebError::BadInputWithSimpleMessage("Media file must be a JPG or PNG image".to_string()));
}
let media_domain = get_media_domain(&http_request);
@@ -167,28 +130,23 @@ 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 {
let args = Hunyuan3d2Args {
image_url: media_links.cdn_url,
webhook_url: &server_state.fal.webhook_url,
api_key: &server_state.fal.api_key,
};
let fal_result = enqueue_hunyuan2_image_to_3d_webhook(args)
let fal_result = enqueue_hunyuan_3d_2_image_to_3d_webhook(args)
.await
.map_err(|err| {
warn!("Error calling enqueue_hunyuan2_image_to_3d_webhook: {:?}", err);
GenerateHunyuan2ImageTo3dError::ServerError
CommonWebError::ServerError
})?;
let external_job_id = fal_result.request_id
.ok_or_else(|| {
warn!("Fal request_id is None");
GenerateHunyuan2ImageTo3dError::ServerError
CommonWebError::ServerError
})?;
info!("Fal request_id: {}", external_job_id);
@@ -211,7 +169,7 @@ pub async fn generate_hunyuan_2_image_to_3d_handler(
Ok(token) => token,
Err(err) => {
warn!("Error inserting generic inference job for FAL queue: {:?}", err);
return Err(GenerateHunyuan2ImageTo3dError::ServerError);
return Err(CommonWebError::ServerError);
}
};

Some files were not shown because too many files have changed in this diff Show More