Compare commits

..
138 changed files with 293 additions and 4610 deletions
Generated
-10
View File
@@ -688,7 +688,6 @@ dependencies = [
"tokio",
"tokio-util",
"url",
"web_base64",
"workspace-build-acceleration",
]
@@ -3598,7 +3597,6 @@ dependencies = [
"fal",
"file_to_base64_url",
"futures 0.3.31",
"log",
"reqwest 0.12.15",
"serde",
"testing",
@@ -12212,14 +12210,6 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "web_base64"
version = "0.0.1"
dependencies = [
"base64 0.22.1",
"workspace-build-acceleration",
]
[[package]]
name = "web_scrapers"
version = "0.0.1"
-2
View File
@@ -43,7 +43,6 @@ 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",
@@ -229,4 +228,3 @@ 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" }
-1
View File
@@ -35,7 +35,6 @@ openai_sora_client = { workspace = true }
primitives = { workspace = true }
storyteller_client = { workspace = true }
tokens = { workspace = true }
web_base64 = { workspace = true }
# External
anyhow = { workspace = true }
@@ -1,48 +0,0 @@
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_image_fal::handle_image_fal;
use crate::core::commands::enqueue::image::handle_image_sora::handle_image_sora;
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::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_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
use fal_client::requests::queue::image_gen::enqueue_flux_pro_ultra_text_to_image::{enqueue_flux_pro_ultra_text_to_image, FluxProUltraTextToImageArgs};
use fal_client::requests::queue::image_gen::enqueue_recraft3_text_to_image::{enqueue_recraft3_text_to_image, Recraft3TextToImageArgs};
use filesys::file_read_bytes::file_read_bytes;
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
@@ -55,8 +55,6 @@ use storyteller_client::media_files::upload_image_media_file_from_file::upload_i
use storyteller_client::utils::api_host::ApiHost;
use tauri::{AppHandle, Emitter, Manager, State};
use tempfile::NamedTempFile;
use crate::core::commands::enqueue::image::handle_image_artcraft::handle_image_artcraft;
use crate::core::commands::enqueue::object::handle_object_artcraft::handle_object_artcraft;
#[derive(Deserialize)]
pub struct EnqueueTextToImageRequest {
@@ -95,8 +93,6 @@ pub enum EnqueueTextToImageErrorType {
NeedsFalApiKey,
/// Fal had an API error
FalError,
/// Needs to be logged into Artcraft
NeedsStorytellerCredentials,
}
@@ -176,20 +172,13 @@ 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_image_sora(&app, request, sora_creds_manager, sora_task_queue).await?;
return Ok(());
}
_ => {
// Fall-through
handle_sora(&app, request, sora_creds_manager, sora_task_queue).await?;
}
};
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(())
}
@@ -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_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
use fal_client::requests::queue::image_gen::enqueue_flux_pro_ultra_text_to_image::{enqueue_flux_pro_ultra_text_to_image, FluxProUltraTextToImageArgs};
use fal_client::requests::queue::image_gen::enqueue_recraft3_text_to_image::{enqueue_recraft3_text_to_image, Recraft3TextToImageArgs};
use log::{error, info, warn};
use tauri::AppHandle;
pub async fn handle_image_fal(
pub async fn handle_fal(
app: &AppHandle,
request: EnqueueTextToImageRequest,
fal_creds_manager: &FalCredentialManager,
@@ -56,7 +56,7 @@ pub async fn handle_image_fal(
}
Some(EnqueueTextToImageModel::FluxProUltra) => {
info!("enqueue Flux Pro Ultra text-to-image with prompt: {}", prompt);
enqueue_flux_pro_11_ultra_text_to_image(FluxPro11UltraTextToImageArgs {
enqueue_flux_pro_ultra_text_to_image(FluxProUltraTextToImageArgs {
prompt,
api_key: &api_key,
}).await
@@ -1,95 +0,0 @@
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,
};
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(())
}
@@ -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_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
use fal_client::requests::queue::image_gen::enqueue_flux_pro_ultra_text_to_image::{enqueue_flux_pro_ultra_text_to_image, FluxProUltraTextToImageArgs};
use fal_client::requests::queue::image_gen::enqueue_recraft3_text_to_image::{enqueue_recraft3_text_to_image, Recraft3TextToImageArgs};
use log::{error, info, warn};
use 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_image_sora(
pub async fn handle_sora(
app: &AppHandle,
request: EnqueueTextToImageRequest,
sora_creds_manager: &SoraCredentialManager,
@@ -1,23 +1,20 @@
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::storyteller_error::StorytellerError;
use storyteller_client::error::api_error::ApiError;
#[derive(Debug)]
pub enum InternalImageError {
NoModelSpecified,
NeedsFalApiKey,
NeedsSoraCredentials,
NeedsStorytellerCredentials,
FalError(FalErrorPlus),
SoraError(SoraError),
AnyhowError(AnyhowError),
StorytellerError(StorytellerError),
StorytellerApiError(ApiError),
DecodeError(DecodeError),
IoError(std::io::Error),
ArtcraftError(ArtcraftError),
}
impl From<AnyhowError> for InternalImageError {
@@ -38,9 +35,9 @@ impl From<SoraError> for InternalImageError {
}
}
impl From<StorytellerError> for InternalImageError {
fn from(value: StorytellerError) -> Self {
Self::StorytellerError(value)
impl From<ApiError> for InternalImageError {
fn from(value: ApiError) -> Self {
Self::StorytellerApiError(value)
}
}
@@ -55,15 +52,3 @@ 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,5 +1,4 @@
pub mod enqueue_text_to_image_command;
pub mod handle_image_artcraft;
pub mod handle_image_fal;
pub mod handle_image_sora;
pub mod handle_fal;
pub mod handle_sora;
pub mod internal_image_error;
@@ -1,3 +1,2 @@
pub mod image;
pub mod object;
pub mod video;
pub mod object;
@@ -1,4 +1,5 @@
use crate::core::commands::enqueue::object::handle_object_artcraft::handle_object_artcraft;
use crate::core::commands::enqueue::image::handle_fal::handle_fal;
use crate::core::commands::enqueue::image::handle_sora::handle_sora;
use crate::core::commands::enqueue::object::handle_object_fal::handle_object_fal;
use crate::core::commands::enqueue::object::internal_object_error::InternalObjectError;
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
@@ -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::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_flux_pro_ultra_text_to_image::{enqueue_flux_pro_ultra_text_to_image, FluxProUltraTextToImageArgs};
use fal_client::requests::queue::image_gen::enqueue_recraft3_text_to_image::{enqueue_recraft3_text_to_image, Recraft3TextToImageArgs};
use filesys::file_read_bytes::file_read_bytes;
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
@@ -56,6 +57,7 @@ use storyteller_client::utils::api_host::ApiHost;
use tauri::{AppHandle, Emitter, Manager, State};
use tempfile::NamedTempFile;
use tokens::tokens::media_files::MediaFileToken;
use crate::core::commands::enqueue::object::handle_object_artcraft::handle_object_artcraft;
#[derive(Deserialize)]
pub struct EnqueueImageTo3dObjectRequest {
@@ -17,7 +17,7 @@ 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_flux_pro_ultra_text_to_image::{enqueue_flux_pro_ultra_text_to_image, FluxProUltraTextToImageArgs};
use fal_client::requests::queue::image_gen::enqueue_recraft3_text_to_image::{enqueue_recraft3_text_to_image, Recraft3TextToImageArgs};
use idempotency::uuid::generate_random_uuid;
use log::{error, info, warn};
@@ -16,7 +16,7 @@ use crate::services::storyteller::state::storyteller_credential_manager::Storyte
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_flux_pro_ultra_text_to_image::{enqueue_flux_pro_ultra_text_to_image, FluxProUltraTextToImageArgs};
use fal_client::requests::queue::image_gen::enqueue_recraft3_text_to_image::{enqueue_recraft3_text_to_image, Recraft3TextToImageArgs};
use log::{error, info, warn};
use tauri::AppHandle;
@@ -34,7 +34,7 @@ pub async fn handle_object_fal(
None => {
error!("No FAL API key is set. Can't perform action.");
let event =
GenerationEnqueueFailureEvent::no_fal_api_key(GenerationAction::ImageTo3d);
GenerationEnqueueFailureEvent::no_fal_api_key(GenerationAction::GenerateImage);
if let Err(err) = event.send(&app) {
error!("Failed to emit event: {:?}", err); // Fail open.
@@ -1,7 +1,7 @@
use crate::core::artcraft_error::ArtcraftError;
use base64::DecodeError;
use errors::AnyhowError;
use fal_client::error::fal_error_plus::FalErrorPlus;
use storyteller_client::error::api_error::ApiError;
use storyteller_client::error::storyteller_error::StorytellerError;
#[derive(Debug)]
@@ -13,9 +13,9 @@ pub enum InternalObjectError {
FalError(FalErrorPlus),
AnyhowError(AnyhowError),
StorytellerError(StorytellerError),
StorytellerApiError(ApiError),
DecodeError(DecodeError),
IoError(std::io::Error),
ArtcraftError(ArtcraftError),
}
impl From<AnyhowError> for InternalObjectError {
@@ -30,9 +30,9 @@ impl From<FalErrorPlus> for InternalObjectError {
}
}
impl From<StorytellerError> for InternalObjectError {
fn from(value: StorytellerError) -> Self {
Self::StorytellerError(value)
impl From<ApiError> for InternalObjectError {
fn from(value: ApiError) -> Self {
Self::StorytellerApiError(value)
}
}
@@ -47,15 +47,3 @@ impl From<std::io::Error> for InternalObjectError {
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),
}
}
}
@@ -1,179 +0,0 @@
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(())
}
@@ -1,138 +0,0 @@
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,
};
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(())
}
@@ -1,124 +0,0 @@
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(())
}
@@ -1,61 +0,0 @@
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),
}
}
}
@@ -1,4 +0,0 @@
pub mod enqueue_image_to_video_command;
pub mod handle_video_artcraft;
pub mod handle_video_fal;
pub mod internal_video_error;
@@ -11,8 +11,6 @@ pub enum GenerationServiceProvider {
#[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")]
-1
View File
@@ -1,4 +1,3 @@
pub mod artcraft_error;
pub mod commands;
pub mod events;
pub mod state;
@@ -33,7 +33,6 @@ 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) -> Result<NamedTempFile, ArtcraftError> {
pub async fn download_media_file_to_temp_dir(app_data_root: &AppDataRoot, token: &MediaFileToken) -> AnyhowResult<NamedTempFile> {
let response = get_media_file(&ApiHost::Storyteller, token).await?;
let media_file_url = &response.media_file.media_links.cdn_url;
@@ -1,12 +1,15 @@
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;
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)?;
// 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)?;
let extension = MimetypeInfo::get_for_bytes(&bytes)
.map(|info| info.file_extension())
-2
View File
@@ -7,7 +7,6 @@ use crate::core::commands::app_preferences::get_app_preferences_command::get_app
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;
@@ -164,7 +163,6 @@ pub fn run() {
.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,4 +1,3 @@
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;
@@ -50,7 +49,6 @@ 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;
@@ -178,10 +176,9 @@ pub async fn fal_background_removal_command(
enum InnerError {
FalError(FalErrorPlus),
AnyhowError(AnyhowError),
StorytellerError(StorytellerError),
StorytellerApiError(ApiError),
DecodeError(DecodeError),
IoError(std::io::Error),
ArtcraftError(ArtcraftError),
}
impl From<AnyhowError> for InnerError {
@@ -196,9 +193,9 @@ impl From<FalErrorPlus> for InnerError {
}
}
impl From<StorytellerError> for InnerError {
fn from(value: StorytellerError) -> Self {
Self::StorytellerError(value)
impl From<ApiError> for InnerError {
fn from(value: ApiError) -> Self {
Self::StorytellerApiError(value)
}
}
@@ -214,17 +211,6 @@ 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,
@@ -238,11 +224,9 @@ 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")));
@@ -253,8 +237,6 @@ 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,4 +1,3 @@
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;
@@ -50,7 +49,6 @@ 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;
@@ -154,10 +152,9 @@ pub async fn fal_hunyuan_image_to_3d_command(
enum InnerError {
FalError(FalErrorPlus),
AnyhowError(AnyhowError),
StorytellerError(StorytellerError),
StorytellerApiError(ApiError),
DecodeError(DecodeError),
IoError(std::io::Error),
ArtcraftError(ArtcraftError),
}
impl From<AnyhowError> for InnerError {
@@ -172,9 +169,9 @@ impl From<FalErrorPlus> for InnerError {
}
}
impl From<StorytellerError> for InnerError {
fn from(value: StorytellerError) -> Self {
Self::StorytellerError(value)
impl From<ApiError> for InnerError {
fn from(value: ApiError) -> Self {
Self::StorytellerApiError(value)
}
}
@@ -190,19 +187,6 @@ 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,
@@ -1,4 +1,3 @@
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;
@@ -25,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::video_gen::enqueue_kling_16_pro_image_to_video::{enqueue_kling_16_pro_image_to_video, Kling16ProArgs, Kling16ProAspectRatio, Kling16ProDuration};
use fal_client::requests::queue::video_gen::enqueue_kling_16_image_to_video::{enqueue_kling_16_image_to_video, Kling16Args, Kling16Duration};
use filesys::file_read_bytes::file_read_bytes;
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
use image::{DynamicImage, EncodableLayout, ImageReader};
@@ -50,7 +49,6 @@ 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;
@@ -159,10 +157,9 @@ pub async fn fal_kling_image_to_video_command(
enum InnerError {
FalError(FalErrorPlus),
AnyhowError(AnyhowError),
StorytellerError(StorytellerError),
StorytellerApiError(ApiError),
DecodeError(DecodeError),
IoError(std::io::Error),
ArtcraftError(ArtcraftError),
}
impl From<AnyhowError> for InnerError {
@@ -177,9 +174,9 @@ impl From<FalErrorPlus> for InnerError {
}
}
impl From<StorytellerError> for InnerError {
fn from(value: StorytellerError) -> Self {
Self::StorytellerError(value)
impl From<ApiError> for InnerError {
fn from(value: ApiError) -> Self {
Self::StorytellerApiError(value)
}
}
@@ -195,18 +192,6 @@ 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,
@@ -233,11 +218,10 @@ pub async fn image_to_video(
let filename = temp_download.path().to_path_buf();
let enqueued = enqueue_kling_16_pro_image_to_video(Kling16ProArgs {
let enqueued = enqueue_kling_16_image_to_video(Kling16Args {
image_path: filename,
api_key: &api_key,
duration: Kling16ProDuration::Default,
aspect_ratio: Kling16ProAspectRatio::WideSixteenNine,
duration: Kling16Duration::Default,
prompt: request.prompt.as_deref().unwrap_or(""),
}).await?;
@@ -39,7 +39,6 @@ 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
@@ -200,7 +199,7 @@ pub async fn sora_image_remix_command(
enum InnerError {
SoraError(SoraError),
AnyhowError(AnyhowError),
StorytellerError(StorytellerError),
StorytellerApiError(ApiError),
}
impl From<AnyhowError> for InnerError {
@@ -215,9 +214,9 @@ impl From<SoraError> for InnerError {
}
}
impl From<StorytellerError> for InnerError {
fn from(value: StorytellerError) -> Self {
Self::StorytellerError(value)
impl From<ApiError> for InnerError {
fn from(value: ApiError) -> Self {
Self::StorytellerApiError(value)
}
}
+2 -3
View File
@@ -16,8 +16,7 @@
"width": 2400,
"height": 1300,
"resizable": true,
"fullscreen": false,
"dragDropEnabled": false
"fullscreen": false
}
],
"security": {
@@ -34,7 +33,7 @@
"https://www.googletagmanager.com",
"ipc://localhost",
"tauri://localhost"
],
],
"connect-src": [
"'self'",
"'unsafe-eval'",
@@ -23,7 +23,6 @@ errors = { workspace = true }
# External
anyhow = { workspace = true }
futures = { workspace = true }
log = { workspace = true }
reqwest = { workspace = true }
serde = { workspace = true }
tokio = { workspace = true }
@@ -7,25 +7,22 @@ use fal::endpoints::fal_ai::flux_pro::v1_1_ultra::FluxProUltraTextToImageInput;
use futures::StreamExt;
use std::io::Write;
pub struct FluxPro11UltraTextToImageArgs<'a> {
pub struct FluxProUltraTextToImageArgs<'a> {
pub prompt: &'a str,
pub api_key: &'a FalApiKey,
// TODO: Size parameter
}
pub async fn enqueue_flux_pro_11_ultra_text_to_image(args: FluxPro11UltraTextToImageArgs<'_>) -> Result<EnqueuedRequest, FalErrorPlus> {
pub async fn enqueue_flux_pro_ultra_text_to_image(args: FluxProUltraTextToImageArgs<'_>) -> Result<EnqueuedRequest, FalErrorPlus> {
let request = FluxProUltraTextToImageInput {
prompt: args.prompt.to_string(),
// 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
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.
aspect_ratio: None, // Default is "16:9"
seed: None,
sync_mode: None, // Synchronous / slow
};
@@ -45,7 +42,7 @@ pub async fn enqueue_flux_pro_11_ultra_text_to_image(args: FluxPro11UltraTextToI
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
use crate::requests::queue::image_gen::enqueue_flux_pro_ultra_text_to_image::{enqueue_flux_pro_ultra_text_to_image, FluxProUltraTextToImageArgs};
use errors::AnyhowResult;
use std::fs::read_to_string;
@@ -57,12 +54,12 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = FluxPro11UltraTextToImageArgs {
let args = FluxProUltraTextToImageArgs {
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_11_ultra_text_to_image(args).await?;
let result = enqueue_flux_pro_ultra_text_to_image(args).await?;
Ok(())
}
@@ -1,2 +1,2 @@
pub mod enqueue_flux_pro_11_ultra_text_to_image;
pub mod enqueue_flux_pro_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,7 +56,6 @@ 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]
@@ -64,9 +63,9 @@ mod tests {
async fn test_remove_background_rembg() -> AnyhowResult<()> {
let image = test_file_path("test_data/image/juno.jpg")?;
// 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);
// TODO: DO NOT COMMIT SECRET !
const KEY : &str = "DO NOT COMMIT";
let api_key = FalApiKey::from_str(KEY);
let result = remove_background_rembg(image, &api_key).await?;
@@ -8,50 +8,35 @@ use futures::StreamExt;
use std::io::Write;
use std::path::Path;
pub struct Kling16ProArgs<'a, P: AsRef<Path>> {
pub struct Kling16Args<'a, P: AsRef<Path>> {
pub image_path: P,
pub prompt: &'a str,
pub api_key: &'a FalApiKey,
pub duration: Kling16ProDuration,
pub aspect_ratio: Kling16ProAspectRatio,
pub duration: Kling16Duration,
}
#[derive(Copy, Clone, Debug)]
pub enum Kling16ProDuration {
pub enum Kling16Duration {
Default,
FiveSeconds,
TenSeconds,
}
#[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> {
pub async fn enqueue_kling_16_image_to_video<P: AsRef<Path>>(args: Kling16Args<'_, P>) -> Result<EnqueuedRequest, FalErrorPlus> {
let image_url = file_to_base64_url(args.image_path)?;
let duration = match args.duration{
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()),
Kling16Duration::Default => None,
Kling16Duration::FiveSeconds => Some("5".to_string()), // Gross...
Kling16Duration::TenSeconds => Some("10".to_string()),
};
let request = ProImageToVideoRequest {
image_url,
prompt: args.prompt.to_string(),
aspect_ratio,
duration,
// Maybe expose these later
aspect_ratio: None,
cfg_scale: None,
duration,
negative_prompt: None,
tail_image_url: None,
};
@@ -72,7 +57,7 @@ pub async fn enqueue_kling_16_pro_image_to_video<P: AsRef<Path>>(args: Kling16Pr
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::queue::video_gen::enqueue_kling_16_pro_image_to_video::{enqueue_kling_16_pro_image_to_video, Kling16ProArgs, Kling16ProAspectRatio, Kling16ProDuration};
use crate::requests::queue::video_gen::enqueue_kling_16_image_to_video::{enqueue_kling_16_image_to_video, Kling16Args, Kling16Duration};
use errors::AnyhowResult;
use std::fs::read_to_string;
use testing::test_file_path::test_file_path;
@@ -87,15 +72,14 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = Kling16ProArgs {
let args = Kling16Args {
image_path: image,
prompt: "a corgi looks out over the water",
api_key: &api_key,
duration: Kling16ProDuration::Default,
aspect_ratio: Kling16ProAspectRatio::Square,
duration: Kling16Duration::Default,
};
let result = enqueue_kling_16_pro_image_to_video(args).await?;
let result = enqueue_kling_16_image_to_video(args).await?;
Ok(())
}
@@ -1,2 +1,2 @@
pub mod enqueue_kling_16_pro_image_to_video;
pub mod enqueue_kling_16_image_to_video;
pub mod enqueue_minimax_01_image_to_video;
@@ -1,106 +0,0 @@
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(())
}
}
@@ -1,105 +0,0 @@
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(())
}
}
@@ -1,104 +0,0 @@
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(())
}
}
@@ -1,39 +0,0 @@
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, 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 async fn enqueue_flux_pro_11_ultra_text_to_image_webhook<U: IntoUrl>(
args: FluxPro11UltraArgs<'_, U>
) -> Result<WebhookResponse, FalErrorPlus> {
let request = FluxProUltraTextToImageInput {
prompt: args.prompt.to_string(),
// Maybe expose
num_images: Some(1), // Default is 1
aspect_ratio: None, // Default is "16:9"
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,5 +1 @@
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;
@@ -5,14 +5,21 @@ use fal::endpoints::fal_ai::hunyuan3d::v2::{v2, Hunyuan3DInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct Hunyuan3d2Args<'a, U: IntoUrl, V: IntoUrl> {
pub struct Hunyuan2Args<'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_image_to_3d_webhook<U: IntoUrl, V: IntoUrl>(
args: Hunyuan3d2Args<'_, U, V>
#[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>
) -> Result<WebhookResponse, FalErrorPlus> {
let image_url = args.image_url.as_str().to_string();
@@ -1,65 +0,0 @@
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(())
}
}
@@ -1,2 +1 @@
pub mod enqueue_hunyuan_3d_2_image_to_3d_webhook;
pub mod enqueue_hunyuan_3d_21_image_to_3d_webhook;
pub mod enqueue_hunyuan2_image_to_3d_webhook;
@@ -5,13 +5,12 @@ 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 Kling16ProArgs<'a, U: IntoUrl, V: IntoUrl> {
pub struct Kling16Args<'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)]
@@ -21,15 +20,8 @@ pub enum Kling16Duration {
TenSeconds,
}
#[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>
pub async fn enqueue_kling_16_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
args: Kling16Args<'_, U, V>
) -> Result<WebhookResponse, FalErrorPlus> {
let duration = match args.duration {
Kling16Duration::Default => None,
@@ -37,21 +29,14 @@ pub async fn enqueue_kling_16_pro_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,
duration,
// Maybe expose these later
aspect_ratio: None,
cfg_scale: None,
duration,
negative_prompt: None,
tail_image_url: None,
};
@@ -1,98 +0,0 @@
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(())
}
}
@@ -1,98 +0,0 @@
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(())
}
}
@@ -1,98 +0,0 @@
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(())
}
}
@@ -1,102 +0,0 @@
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,5 +1 @@
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;
pub mod enqueue_kling_16_image_to_video_webhook;
@@ -1,68 +0,0 @@
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)
}
@@ -1 +0,0 @@
pub mod generate_flux_pro_11_ultra_text_to_image;
@@ -1,3 +1 @@
pub mod image;
pub mod object;
pub mod video;
pub mod object;
@@ -9,6 +9,7 @@ use artcraft_api_defs::generate::object::generate_hunyuan_2_image_to_3d::{Genera
use log::{debug, info};
use reqwest::Client;
/// Upload an image media file from a file.
pub async fn generate_hunyuan2_image_to_3d(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
@@ -1,68 +0,0 @@
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)
}
@@ -1 +0,0 @@
pub mod generate_kling_16_pro_image_to_video;
@@ -1,5 +1,4 @@
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;
@@ -24,18 +23,16 @@ 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, StorytellerError> {
pub async fn get_media_file(api_host: &ApiHost, media_file_token: &MediaFileToken) -> Result<GetMediaFileSuccessResponse, ApiError> {
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
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
let response_body = &response.text().await?;
let media_file = serde_json::from_str(&response_body)
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
let media_file = serde_json::from_str(&response_body)?;
Ok(media_file)
}
@@ -1,7 +1,5 @@
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;
@@ -38,7 +36,7 @@ pub async fn upload_image_media_file_from_file<P: AsRef<Path>>(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
path: P,
) -> Result<UploadImageMediaFileSuccessResponse, StorytellerError> {
) -> Result<UploadImageMediaFileSuccessResponse, ApiError> {
let url = get_route(api_host);
@@ -48,14 +46,12 @@ pub async fn upload_image_media_file_from_file<P: AsRef<Path>>(
let client = Client::builder()
.gzip(true)
.build()
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
.build()?;
let form = Form::new()
.text("uuid_idempotency_token", generate_random_uuid())
.file("file", path)
.await
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
.await?;
let mut request_builder = client.post(url)
.header("User-Agent", USER_AGENT)
@@ -70,15 +66,12 @@ pub async fn upload_image_media_file_from_file<P: AsRef<Path>>(
let response = request_builder
.multipart(form)
.send()
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
.await?;
let response = filter_bad_response(response).await?;
let response_body = &response.text().await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
let response_body = &response.text().await?;
let media_file = serde_json::from_str(&response_body)
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
let media_file = serde_json::from_str(&response_body)?;
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(err),
Err(err) => Err(StorytellerError::Api(err)),
}
}
Some(_) => Err(StorytellerError::Client(ClientError::FileTypeNotHandled(path.as_ref().to_path_buf()))),
-25
View File
@@ -1,25 +0,0 @@
[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
@@ -1,5 +0,0 @@
//! 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;
@@ -1,14 +0,0 @@
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)
}
@@ -1,20 +0,0 @@
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>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GenerateFluxPro11UltraTextToImageResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -1,2 +1 @@
pub mod generate_flux_pro_11_ultra_text_to_image;
pub mod remove_image_background;
@@ -3,10 +3,10 @@ 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";
const GENERATE_KLING_1_6_IMAGE_TO_VIDEO_URL_PATH: &str = "/v1/generate/video/kling_16_image_to_video";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GenerateKling16ProImageToVideoRequest {
pub struct GenerateKling16ImageToVideoRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
@@ -16,23 +16,10 @@ pub struct GenerateKling16ProImageToVideoRequest {
/// Optional text prompt.
pub prompt: Option<String>,
/// Optional: apsect ratio of the generated video.
pub aspect_ratio: Option<GenerateKling16ProAspectRatio>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub enum GenerateKling16ProAspectRatio {
/// 16:9 aspect ratio
WideSixteenNine,
/// 9:16 aspect ratio
TallNineSixteen,
/// 1:1 aspect ratio
Square,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GenerateKling16ProImageToVideoResponse {
pub struct GenerateKling16ImageToVideoResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -1 +1 @@
pub mod generate_kling_1_6_pro_image_to_video;
pub mod generate_kling_1_6_image_to_video;
@@ -1,175 +0,0 @@
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::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::GenerateFluxPro11UltraTextToImageRequest;
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageResponse;
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::enqueue_flux_pro_11_ultra_text_to_image_webhook;
use fal_client::requests::webhook::image::enqueue_flux_pro_11_ultra_text_to_image_webhook::FluxPro11UltraArgs;
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;
// =============== Error Response ===============
#[derive(Debug, Serialize, ToSchema)]
pub enum GenerateFluxProUltraTextToImageError {
BadInput(String),
NotFound,
NotAuthorized,
ServerError,
}
impl ResponseError for GenerateFluxProUltraTextToImageError {
fn status_code(&self) -> StatusCode {
match *self {
GenerateFluxProUltraTextToImageError::BadInput(_) => StatusCode::BAD_REQUEST,
GenerateFluxProUltraTextToImageError::NotFound => StatusCode::NOT_FOUND,
GenerateFluxProUltraTextToImageError::NotAuthorized => StatusCode::UNAUTHORIZED,
GenerateFluxProUltraTextToImageError::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 GenerateFluxProUltraTextToImageError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)
}
}
// =============== Handler ===============
/// Flux pro ultra text to image
#[utoipa::path(
post,
tag = "Generate Images",
path = "/v1/generate/image/flux_pro_1.1_ultra_text_to_image",
responses(
(status = 200, description = "Success", body = GenerateFluxProUltraTextToImageResponse),
(status = 400, description = "Bad input", body = GenerateFluxProUltraTextToImageError),
(status = 401, description = "Not authorized", body = GenerateFluxProUltraTextToImageError),
(status = 500, description = "Server error", body = GenerateFluxProUltraTextToImageError),
),
params(
("request" = GenerateFluxProUltraTextToImageRequest, 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>, GenerateFluxProUltraTextToImageError> {
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);
GenerateFluxProUltraTextToImageError::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(GenerateFluxProUltraTextToImageError::BadInput(reason));
}
insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool)
.await
.map_err(|err| {
error!("Error inserting idempotency token: {:?}", err);
GenerateFluxProUltraTextToImageError::BadInput("invalid idempotency token".to_string())
})?;
const IS_MOD : bool = false;
info!("Fal webhook URL: {}", server_state.fal.webhook_url);
let args = FluxPro11UltraArgs {
prompt: request.prompt.as_deref().unwrap_or(""),
webhook_url: &server_state.fal.webhook_url,
api_key: &server_state.fal.api_key,
};
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);
GenerateFluxProUltraTextToImageError::ServerError
})?;
let external_job_id = fal_result.request_id
.ok_or_else(|| {
warn!("Fal request_id is None");
GenerateFluxProUltraTextToImageError::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(GenerateFluxProUltraTextToImageError::ServerError);
}
};
Ok(Json(GenerateFluxPro11UltraTextToImageResponse {
success: true,
inference_job_token: job_token,
}))
}
@@ -1,2 +1 @@
pub mod generate_flux_pro_ultra_text_to_image_handler;
pub mod remove_image_background_handler;
@@ -19,8 +19,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_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 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 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;
@@ -167,13 +167,13 @@ pub async fn generate_hunyuan_2_image_to_3d_handler(
info!("Fal webhook URL: {}", server_state.fal.webhook_url);
let args = Hunyuan3d2Args {
let args = Hunyuan2Args {
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_image_to_3d_webhook(args)
let fal_result = enqueue_hunyuan2_image_to_3d_webhook(args)
.await
.map_err(|err| {
warn!("Error calling enqueue_hunyuan2_image_to_3d_webhook: {:?}", err);
@@ -15,14 +15,13 @@ 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::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProImageToVideoResponse;
use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::{GenerateKling16ProAspectRatio, GenerateKling16ProImageToVideoRequest};
use artcraft_api_defs::generate::video::generate_kling_1_6_image_to_video::GenerateKling16ImageToVideoRequest;
use artcraft_api_defs::generate::video::generate_kling_1_6_image_to_video::GenerateKling16ImageToVideoResponse;
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use enums::common::visibility::Visibility;
use fal_client::requests::webhook::video::enqueue_kling_16_pro_image_to_video_webhook::enqueue_kling_16_pro_image_to_video_webhook;
use fal_client::requests::webhook::video::enqueue_kling_16_pro_image_to_video_webhook::Kling16Duration;
use fal_client::requests::webhook::video::enqueue_kling_16_pro_image_to_video_webhook::Kling16ProArgs;
use fal_client::requests::webhook::video::enqueue_kling_16_pro_image_to_video_webhook::Kling16ProAspectRatio;
use fal_client::requests::webhook::video::enqueue_kling_16_image_to_video_webhook::enqueue_kling_16_image_to_video_webhook;
use fal_client::requests::webhook::video::enqueue_kling_16_image_to_video_webhook::Kling16Args;
use fal_client::requests::webhook::video::enqueue_kling_16_image_to_video_webhook::Kling16Duration;
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;
@@ -38,29 +37,6 @@ use utoipa::ToSchema;
// =============== Error Response ===============
/*
TODO: Either on the Tauri client or as an API, make "generic" image / video / object endpoints.
Take a reference image payload:
ReferenceImage {
media_token: MediaFileToken,
reference_type: ReferenceType
}
enum ReferenceType {
PrimaryReference,
CharacterReference,
LocationReference,
StyleReference,
...
GenericReference,
}
// **ORDER MATTERS**
reference_images: Vec<ReferenceImage>,
*/
#[derive(Debug, Serialize, ToSchema)]
pub enum GenerateKling16VideoError {
BadInput(String),
@@ -97,7 +73,7 @@ impl fmt::Display for GenerateKling16VideoError {
#[utoipa::path(
post,
tag = "Generate Videos",
path = "/v1/generate/video/kling_1.6_pro_image_to_video",
path = "/v1/generate/video/kling_16_image_to_video",
responses(
(status = 200, description = "Success", body = RemoveImageBackgroundResponse),
(status = 400, description = "Bad input", body = RemoveImageBackgroundError),
@@ -108,11 +84,11 @@ impl fmt::Display for GenerateKling16VideoError {
("request" = RemoveImageBackgroundRequest, description = "Payload for Request"),
)
)]
pub async fn generate_kling_1_6_pro_video_handler(
pub async fn generate_kling_1_6_video_handler(
http_request: HttpRequest,
request: Json<GenerateKling16ProImageToVideoRequest>,
request: Json<GenerateKling16ImageToVideoRequest>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<GenerateKling16ProImageToVideoResponse>, GenerateKling16VideoError> {
) -> Result<Json<GenerateKling16ImageToVideoResponse>, GenerateKling16VideoError> {
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
@@ -197,23 +173,15 @@ pub async fn generate_kling_1_6_pro_video_handler(
.map(|prompt| prompt.trim())
.unwrap_or_else(|| "");
let aspect_ratio = match &request.aspect_ratio {
Some(GenerateKling16ProAspectRatio::Square) => Kling16ProAspectRatio::Square,
Some(GenerateKling16ProAspectRatio::WideSixteenNine) => Kling16ProAspectRatio::WideSixteenNine,
Some(GenerateKling16ProAspectRatio::TallNineSixteen) => Kling16ProAspectRatio::TallNineSixteen,
None => Kling16ProAspectRatio::WideSixteenNine, // Default to 16:9
};
let args = Kling16ProArgs {
let args = Kling16Args {
image_url: media_links.cdn_url,
webhook_url: &server_state.fal.webhook_url,
duration: Kling16Duration::Default,
prompt: prompt,
aspect_ratio: aspect_ratio,
api_key: &server_state.fal.api_key,
};
let fal_result = enqueue_kling_16_pro_image_to_video_webhook(args)
let fal_result = enqueue_kling_16_image_to_video_webhook(args)
.await
.map_err(|err| {
warn!("Error calling remove_background_rembg_webhook: {:?}", err);
@@ -250,7 +218,7 @@ pub async fn generate_kling_1_6_pro_video_handler(
}
};
Ok(Json(GenerateKling16ProImageToVideoResponse {
Ok(Json(GenerateKling16ImageToVideoResponse {
success: true,
inference_job_token: job_token,
}))
@@ -1 +1 @@
pub mod generate_kling_1_6_pro_video_handler;
pub mod generate_kling_1_6_video_handler;
@@ -1,7 +1,6 @@
use crate::http_server::endpoints::generate::image::generate_flux_pro_ultra_text_to_image_handler::generate_flux_pro_11_ultra_text_to_image_handler;
use crate::http_server::endpoints::generate::image::remove_image_background_handler::remove_image_background_handler;
use crate::http_server::endpoints::generate::object::generate_hunyuan2_image_to_3d_handler::generate_hunyuan_2_image_to_3d_handler;
use crate::http_server::endpoints::generate::video::generate_kling_1_6_pro_video_handler::generate_kling_1_6_pro_video_handler;
use crate::http_server::endpoints::generate::video::generate_kling_1_6_video_handler::generate_kling_1_6_video_handler;
use actix_http::body::MessageBody;
use actix_service::ServiceFactory;
use actix_web::dev::{ServiceRequest, ServiceResponse};
@@ -20,18 +19,14 @@ where
{
app.service(web::scope("/v1/generate")
.service(web::scope("/image")
.service(web::resource("/flux_pro_1.1_ultra_text_to_image")
.route(web::post().to(generate_flux_pro_11_ultra_text_to_image_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
.service(web::resource("/remove_background")
.route(web::post().to(remove_image_background_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
)
.service(web::scope("/video")
.service(web::resource("/kling_1.6_pro_image_to_video")
.route(web::post().to(generate_kling_1_6_pro_video_handler))
.service(web::resource("/kling_16_image_to_video")
.route(web::post().to(generate_kling_1_6_video_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
)
-1
View File
@@ -86,7 +86,6 @@ endpoints_fal-ai_auto-caption = []
endpoints_fal-ai_ben = []
endpoints_fal-ai_birefnet = []
endpoints_fal-ai_bria = []
endpoints_fal-ai_bytedance = []
endpoints_fal-ai_cat-vton = []
endpoints_fal-ai_ccsr = []
endpoints_fal-ai_clarity-upscaler = []
@@ -1,14 +0,0 @@
#[cfg(any(
feature = "endpoints",
feature = "endpoints_fal-ai",
feature = "endpoints_fal-ai_bytedance"
))]
#[cfg_attr(
docsrs,
doc(cfg(any(
feature = "endpoints",
feature = "endpoints_fal-ai",
feature = "endpoints_fal-ai_bytedance"
)))
)]
pub mod seedance;
@@ -1,14 +0,0 @@
#[cfg(any(
feature = "endpoints",
feature = "endpoints_fal-ai",
feature = "endpoints_fal-ai_bytedance"
))]
#[cfg_attr(
docsrs,
doc(cfg(any(
feature = "endpoints",
feature = "endpoints_fal-ai",
feature = "endpoints_fal-ai_bytedance"
)))
)]
pub mod v1;
@@ -1,69 +0,0 @@
#[allow(unused_imports)]
use crate::prelude::*;
#[allow(unused_imports)]
use serde::{Deserialize, Serialize};
#[allow(unused_imports)]
use std::collections::HashMap;
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct File {
/// The mime type of the file.
/// "image/png"
#[serde(skip_serializing_if = "Option::is_none")]
pub content_type: Option<String>,
/// File data
#[serde(skip_serializing_if = "Option::is_none")]
pub file_data: Option<String>,
/// The name of the file. It will be auto-generated if not provided.
/// "z9RV14K95DvU.png"
#[serde(skip_serializing_if = "Option::is_none")]
pub file_name: Option<String>,
/// The size of the file in bytes.
/// 4404019
#[serde(skip_serializing_if = "Option::is_none")]
pub file_size: Option<i64>,
/// The URL where the file can be downloaded from.
pub url: String,
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct HTTPValidationError {
#[serde(skip_serializing_if = "Option::is_none")]
pub detail: Option<Vec<Option<ValidationError>>>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct I2VOutput {
/// The generated video
/// {"url":"https://v2.fal.media/files/36087878b0c1435bb75c19b64b7db178_output.mp4"}
pub video: File,
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct ImageToVideoRequest {
/// The resolution of the generated video frame
#[serde(skip_serializing_if = "Option::is_none")]
pub resolution: Option<String>,
/// The duration of the generated video in seconds
#[serde(skip_serializing_if = "Option::is_none")]
pub duration: Option<String>,
pub image_url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub camera_fixed: Option<bool>,
pub prompt: String,
pub seed: i32,
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct ValidationError {
pub loc: Vec<serde_json::Value>,
pub msg: String,
#[serde(rename = "type")]
pub ty: String,
}
pub fn image_to_video(
params: ImageToVideoRequest,
) -> FalRequest<ImageToVideoRequest, I2VOutput> {
FalRequest::new("fal-ai/bytedance/seedance/v1/lite/image-to-video", params)
}
@@ -1,14 +0,0 @@
#[cfg(any(
feature = "endpoints",
feature = "endpoints_fal-ai",
feature = "endpoints_fal-ai_bytedance"
))]
#[cfg_attr(
docsrs,
doc(cfg(any(
feature = "endpoints",
feature = "endpoints_fal-ai",
feature = "endpoints_fal-ai_bytedance"
)))
)]
pub mod image_to_video;
@@ -1,14 +0,0 @@
#[cfg(any(
feature = "endpoints",
feature = "endpoints_fal-ai",
feature = "endpoints_fal-ai_bytedance"
))]
#[cfg_attr(
docsrs,
doc(cfg(any(
feature = "endpoints",
feature = "endpoints_fal-ai",
feature = "endpoints_fal-ai_bytedance"
)))
)]
pub mod lite;
@@ -12,17 +12,3 @@
)))
)]
pub mod v2;
#[cfg(any(
feature = "endpoints",
feature = "endpoints_fal-ai",
feature = "endpoints_fal-ai_hunyuan3d"
))]
#[cfg_attr(
docsrs,
doc(cfg(any(
feature = "endpoints",
feature = "endpoints_fal-ai",
feature = "endpoints_fal-ai_hunyuan3d"
)))
)]
pub mod v21;
@@ -1,64 +0,0 @@
use crate::prelude::FalRequest;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct Hunyuan3DInput {
/// Guidance scale for the model.
#[serde(skip_serializing_if = "Option::is_none")]
pub guidance_scale: Option<f64>,
/// URL of image to use while generating the 3D model.
/// "https://storage.googleapis.com/falserverless/model_tests/video_models/robot.png"
pub input_image_url: String,
/// Number of inference steps to perform.
#[serde(skip_serializing_if = "Option::is_none")]
pub num_inference_steps: Option<i64>,
/// Octree resolution for the model.
#[serde(skip_serializing_if = "Option::is_none")]
pub octree_resolution: Option<i64>,
/// The same seed and the same prompt given to the same version of the model
/// will output the same image every time.
#[serde(skip_serializing_if = "Option::is_none")]
pub seed: Option<i64>,
/// If set true, textured mesh will be generated and the price charged would be 3 times that of white mesh.
#[serde(skip_serializing_if = "Option::is_none")]
pub textured_mesh: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ObjectOutput {
/// Generated 3D object file.
/// {"content_type":"application/octet-stream","file_name":"white_mesh.glb","file_size":720696,"url":"https://v3.fal.media/files/lion/WqIhtKPaSoeBtC30qzIGG_white_mesh.glb"}
pub model_mesh: File,
/// Seed value used for generation.
pub seed: i64,
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct File {
/// The mime type of the file.
/// "image/png"
#[serde(skip_serializing_if = "Option::is_none")]
pub content_type: Option<String>,
/// File data
#[serde(skip_serializing_if = "Option::is_none")]
pub file_data: Option<String>,
/// The name of the file. It will be auto-generated if not provided.
/// "z9RV14K95DvU.png"
#[serde(skip_serializing_if = "Option::is_none")]
pub file_name: Option<String>,
/// The size of the file in bytes.
/// 4404019
#[serde(skip_serializing_if = "Option::is_none")]
pub file_size: Option<i64>,
/// The URL where the file can be downloaded from.
pub url: String,
}
/// Hunyuan3D
///
/// Category: image-to-3d
/// Machine Type: H100
/// License Type: commercial
pub fn v21(params: Hunyuan3DInput) -> FalRequest<Hunyuan3DInput, ObjectOutput> {
FalRequest::new("fal-ai/hunyuan3d-v21", params)
}
@@ -40,17 +40,3 @@ pub mod v1_5;
)))
)]
pub mod v1_6;
#[cfg(any(
feature = "endpoints",
feature = "endpoints_fal-ai",
feature = "endpoints_fal-ai_kling-video"
))]
#[cfg_attr(
docsrs,
doc(cfg(any(
feature = "endpoints",
feature = "endpoints_fal-ai",
feature = "endpoints_fal-ai_kling-video"
)))
)]
pub mod v2_1;
@@ -1,241 +0,0 @@
#[allow(unused_imports)]
use crate::prelude::*;
#[allow(unused_imports)]
use serde::{Deserialize, Serialize};
#[allow(unused_imports)]
use std::collections::HashMap;
// #[derive(Debug, Serialize, Deserialize, Default)]
// pub struct CameraControl {
// /// The type of camera movement
// /// "horizontal"
// pub movement_type: String,
// /// The value of the camera movement
// /// 10
// pub movement_value: i64,
// }
// #[derive(Debug, Serialize, Deserialize, Default)]
// pub struct DynamicMask {
// /// URL of the image for Dynamic Brush Application Area (Mask image created by users using the motion brush)
// /// "https://h2.inkwai.com/bs2/upload-ylab-stunt/ai_portal/1732888130/WU8spl23dA/dynamic_mask_1.png"
// pub mask_url: String,
// /// List of trajectories
// /// [{"x":279,"y":219},{"x":417,"y":65}]
// #[serde(skip_serializing_if = "Option::is_none")]
// pub trajectories: Option<Vec<Option<Trajectory>>>,
// }
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct File {
/// The mime type of the file.
/// "image/png"
#[serde(skip_serializing_if = "Option::is_none")]
pub content_type: Option<String>,
/// File data
#[serde(skip_serializing_if = "Option::is_none")]
pub file_data: Option<String>,
/// The name of the file. It will be auto-generated if not provided.
/// "z9RV14K95DvU.png"
#[serde(skip_serializing_if = "Option::is_none")]
pub file_name: Option<String>,
/// The size of the file in bytes.
/// 4404019
#[serde(skip_serializing_if = "Option::is_none")]
pub file_size: Option<i64>,
/// The URL where the file can be downloaded from.
pub url: String,
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct HTTPValidationError {
#[serde(skip_serializing_if = "Option::is_none")]
pub detail: Option<Vec<Option<ValidationError>>>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct I2VOutput {
/// The generated video
/// {"url":"https://v2.fal.media/files/36087878b0c1435bb75c19b64b7db178_output.mp4"}
pub video: File,
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct ImageToVideoRequest {
/// The aspect ratio of the generated video frame
#[serde(skip_serializing_if = "Option::is_none")]
pub aspect_ratio: Option<String>,
/// The CFG (Classifier Free Guidance) scale is a measure of how close you want
/// the model to stick to your prompt.
#[serde(skip_serializing_if = "Option::is_none")]
pub cfg_scale: Option<f64>,
/// The duration of the generated video in seconds
#[serde(skip_serializing_if = "Option::is_none")]
pub duration: Option<String>,
pub image_url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub negative_prompt: Option<String>,
pub prompt: String,
}
// #[derive(Debug, Serialize, Deserialize, Default)]
// pub struct KlingV1I2VOutput {
// /// The generated video
// /// {"url":"https://v3.fal.media/files/kangaroo/VUmAU3JvzS7mxwdgSU9zj_output.mp4"}
// pub video: File,
// }
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct MasterImageToVideoRequest {
/// The aspect ratio of the generated video frame
#[serde(skip_serializing_if = "Option::is_none")]
pub aspect_ratio: Option<String>,
/// The CFG (Classifier Free Guidance) scale is a measure of how close you want
/// the model to stick to your prompt.
#[serde(skip_serializing_if = "Option::is_none")]
pub cfg_scale: Option<f64>,
/// The duration of the generated video in seconds
#[serde(skip_serializing_if = "Option::is_none")]
pub duration: Option<String>,
pub image_url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub negative_prompt: Option<String>,
pub prompt: String,
/// URL of the image to be used for the end of the video
#[serde(skip_serializing_if = "Option::is_none")]
pub tail_image_url: Option<String>,
}
// #[derive(Debug, Serialize, Deserialize, Default)]
// pub struct T2VOutput {
// /// The generated video
// /// {"url":"https://v2.fal.media/files/fb33a862b94d4d7195e610e4cbc5d392_output.mp4"}
// pub video: File,
// }
//
// #[derive(Debug, Serialize, Deserialize, Default)]
// pub struct TextToVideoRequest {
// /// The aspect ratio of the generated video frame
// #[serde(skip_serializing_if = "Option::is_none")]
// pub aspect_ratio: Option<String>,
// /// The CFG (Classifier Free Guidance) scale is a measure of how close you want
// /// the model to stick to your prompt.
// #[serde(skip_serializing_if = "Option::is_none")]
// pub cfg_scale: Option<f64>,
// /// The duration of the generated video in seconds
// #[serde(skip_serializing_if = "Option::is_none")]
// pub duration: Option<String>,
// #[serde(skip_serializing_if = "Option::is_none")]
// pub negative_prompt: Option<String>,
// pub prompt: String,
// }
//
// #[derive(Debug, Serialize, Deserialize, Default)]
// pub struct Trajectory {
// /// X coordinate of the motion trajectory
// /// 279
// pub x: i64,
// /// Y coordinate of the motion trajectory
// /// 219
// pub y: i64,
// }
//
// #[derive(Debug, Serialize, Deserialize, Default)]
// pub struct V1ImageToVideoRequest {
// /// The aspect ratio of the generated video frame
// #[serde(skip_serializing_if = "Option::is_none")]
// pub aspect_ratio: Option<String>,
// /// The CFG (Classifier Free Guidance) scale is a measure of how close you want
// /// the model to stick to your prompt.
// #[serde(skip_serializing_if = "Option::is_none")]
// pub cfg_scale: Option<f64>,
// /// The duration of the generated video in seconds
// #[serde(skip_serializing_if = "Option::is_none")]
// pub duration: Option<String>,
// /// List of dynamic masks
// #[serde(skip_serializing_if = "Option::is_none")]
// pub dynamic_masks: Option<Vec<Option<DynamicMask>>>,
// /// URL of the image to be used for the video
// /// "https://h2.inkwai.com/bs2/upload-ylab-stunt/se/ai_portal_queue_mmu_image_upscale_aiweb/3214b798-e1b4-4b00-b7af-72b5b0417420_raw_image_0.jpg"
// pub image_url: String,
// #[serde(skip_serializing_if = "Option::is_none")]
// pub negative_prompt: Option<String>,
// /// The prompt for the video
// /// "The astronaut stood up and walked away"
// pub prompt: String,
// /// URL of the image for Static Brush Application Area (Mask image created by users using the motion brush)
// /// "https://h2.inkwai.com/bs2/upload-ylab-stunt/ai_portal/1732888177/cOLNrShrSO/static_mask.png"
// #[serde(skip_serializing_if = "Option::is_none")]
// pub static_mask_url: Option<String>,
// /// URL of the image to be used for the end of the video
// #[serde(skip_serializing_if = "Option::is_none")]
// pub tail_image_url: Option<String>,
// }
//
// #[derive(Debug, Serialize, Deserialize, Default)]
// pub struct V1TextToVideoRequest {
// /// Advanced Camera control parameters
// #[serde(skip_serializing_if = "Option::is_none")]
// pub advanced_camera_control: Option<Option<CameraControl>>,
// /// The aspect ratio of the generated video frame
// #[serde(skip_serializing_if = "Option::is_none")]
// pub aspect_ratio: Option<String>,
// /// Camera control parameters
// #[serde(skip_serializing_if = "Option::is_none")]
// pub camera_control: Option<String>,
// /// The CFG (Classifier Free Guidance) scale is a measure of how close you want
// /// the model to stick to your prompt.
// #[serde(skip_serializing_if = "Option::is_none")]
// pub cfg_scale: Option<f64>,
// /// The duration of the generated video in seconds
// #[serde(skip_serializing_if = "Option::is_none")]
// pub duration: Option<String>,
// #[serde(skip_serializing_if = "Option::is_none")]
// pub negative_prompt: Option<String>,
// pub prompt: String,
// }
//
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct ValidationError {
pub loc: Vec<serde_json::Value>,
pub msg: String,
#[serde(rename = "type")]
pub ty: String,
}
//
// #[derive(Debug, Serialize, Deserialize, Default)]
// pub struct VideoEffectsOutput {
// /// The generated video
// /// {"content_type":"video/mp4","file_name":"output.mp4","url":"https://storage.googleapis.com/falserverless/kling/kling_ex.mp4.mp4"}
// pub video: File,
// }
//
// #[derive(Debug, Serialize, Deserialize, Default)]
// pub struct VideoEffectsRequest {
// /// The duration of the generated video in seconds
// #[serde(skip_serializing_if = "Option::is_none")]
// pub duration: Option<String>,
// /// The effect scene to use for the video generation
// /// "hug"
// pub effect_scene: String,
// /// URL of the image to be used for the squish and expansion video
// /// "https://h2.inkwai.com/bs2/upload-ylab-stunt/se/ai_portal_queue_mmu_image_upscale_aiweb/3214b798-e1b4-4b00-b7af-72b5b0417420_raw_image_0.jpg"
// pub image_url: String,
// /// URL of images to be used for hug, kiss or heart_gesture video.
// /// ["https://storage.googleapis.com/falserverless/juggernaut_examples/VHXMavzPyI27zi6JseyL4.png","https://storage.googleapis.com/falserverless/juggernaut_examples/QEW5VrzccxGva7mPfEXjf.png"]
// #[serde(skip_serializing_if = "Option::is_none")]
// pub input_image_urls: Option<Vec<Option<String>>>,
// }
/// Kling 1.0
///
/// Category: text-to-video
/// Machine Type: A100
///
///
/// Kling 1.6 (pro) Image to Video API.
pub fn image_to_video(
params: MasterImageToVideoRequest,
) -> FalRequest<MasterImageToVideoRequest, I2VOutput> {
FalRequest::new("fal-ai/kling-video/v2.1/master/image-to-video", params)
}
@@ -1,14 +0,0 @@
#[cfg(any(
feature = "endpoints",
feature = "endpoints_fal-ai",
feature = "endpoints_fal-ai_kling-video"
))]
#[cfg_attr(
docsrs,
doc(cfg(any(
feature = "endpoints",
feature = "endpoints_fal-ai",
feature = "endpoints_fal-ai_kling-video"
)))
)]
pub mod image_to_video;
@@ -1,28 +0,0 @@
#[cfg(any(
feature = "endpoints",
feature = "endpoints_fal-ai",
feature = "endpoints_fal-ai_kling-video"
))]
#[cfg_attr(
docsrs,
doc(cfg(any(
feature = "endpoints",
feature = "endpoints_fal-ai",
feature = "endpoints_fal-ai_kling-video"
)))
)]
pub mod master;
#[cfg(any(
feature = "endpoints",
feature = "endpoints_fal-ai",
feature = "endpoints_fal-ai_kling-video"
))]
#[cfg_attr(
docsrs,
doc(cfg(any(
feature = "endpoints",
feature = "endpoints_fal-ai",
feature = "endpoints_fal-ai_kling-video"
)))
)]
pub mod pro;
@@ -1,241 +0,0 @@
#[allow(unused_imports)]
use crate::prelude::*;
#[allow(unused_imports)]
use serde::{Deserialize, Serialize};
#[allow(unused_imports)]
use std::collections::HashMap;
// #[derive(Debug, Serialize, Deserialize, Default)]
// pub struct CameraControl {
// /// The type of camera movement
// /// "horizontal"
// pub movement_type: String,
// /// The value of the camera movement
// /// 10
// pub movement_value: i64,
// }
// #[derive(Debug, Serialize, Deserialize, Default)]
// pub struct DynamicMask {
// /// URL of the image for Dynamic Brush Application Area (Mask image created by users using the motion brush)
// /// "https://h2.inkwai.com/bs2/upload-ylab-stunt/ai_portal/1732888130/WU8spl23dA/dynamic_mask_1.png"
// pub mask_url: String,
// /// List of trajectories
// /// [{"x":279,"y":219},{"x":417,"y":65}]
// #[serde(skip_serializing_if = "Option::is_none")]
// pub trajectories: Option<Vec<Option<Trajectory>>>,
// }
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct File {
/// The mime type of the file.
/// "image/png"
#[serde(skip_serializing_if = "Option::is_none")]
pub content_type: Option<String>,
/// File data
#[serde(skip_serializing_if = "Option::is_none")]
pub file_data: Option<String>,
/// The name of the file. It will be auto-generated if not provided.
/// "z9RV14K95DvU.png"
#[serde(skip_serializing_if = "Option::is_none")]
pub file_name: Option<String>,
/// The size of the file in bytes.
/// 4404019
#[serde(skip_serializing_if = "Option::is_none")]
pub file_size: Option<i64>,
/// The URL where the file can be downloaded from.
pub url: String,
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct HTTPValidationError {
#[serde(skip_serializing_if = "Option::is_none")]
pub detail: Option<Vec<Option<ValidationError>>>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct I2VOutput {
/// The generated video
/// {"url":"https://v2.fal.media/files/36087878b0c1435bb75c19b64b7db178_output.mp4"}
pub video: File,
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct ImageToVideoRequest {
/// The aspect ratio of the generated video frame
#[serde(skip_serializing_if = "Option::is_none")]
pub aspect_ratio: Option<String>,
/// The CFG (Classifier Free Guidance) scale is a measure of how close you want
/// the model to stick to your prompt.
#[serde(skip_serializing_if = "Option::is_none")]
pub cfg_scale: Option<f64>,
/// The duration of the generated video in seconds
#[serde(skip_serializing_if = "Option::is_none")]
pub duration: Option<String>,
pub image_url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub negative_prompt: Option<String>,
pub prompt: String,
}
// #[derive(Debug, Serialize, Deserialize, Default)]
// pub struct KlingV1I2VOutput {
// /// The generated video
// /// {"url":"https://v3.fal.media/files/kangaroo/VUmAU3JvzS7mxwdgSU9zj_output.mp4"}
// pub video: File,
// }
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct ProImageToVideoRequest {
/// The aspect ratio of the generated video frame
#[serde(skip_serializing_if = "Option::is_none")]
pub aspect_ratio: Option<String>,
/// The CFG (Classifier Free Guidance) scale is a measure of how close you want
/// the model to stick to your prompt.
#[serde(skip_serializing_if = "Option::is_none")]
pub cfg_scale: Option<f64>,
/// The duration of the generated video in seconds
#[serde(skip_serializing_if = "Option::is_none")]
pub duration: Option<String>,
pub image_url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub negative_prompt: Option<String>,
pub prompt: String,
/// URL of the image to be used for the end of the video
#[serde(skip_serializing_if = "Option::is_none")]
pub tail_image_url: Option<String>,
}
// #[derive(Debug, Serialize, Deserialize, Default)]
// pub struct T2VOutput {
// /// The generated video
// /// {"url":"https://v2.fal.media/files/fb33a862b94d4d7195e610e4cbc5d392_output.mp4"}
// pub video: File,
// }
//
// #[derive(Debug, Serialize, Deserialize, Default)]
// pub struct TextToVideoRequest {
// /// The aspect ratio of the generated video frame
// #[serde(skip_serializing_if = "Option::is_none")]
// pub aspect_ratio: Option<String>,
// /// The CFG (Classifier Free Guidance) scale is a measure of how close you want
// /// the model to stick to your prompt.
// #[serde(skip_serializing_if = "Option::is_none")]
// pub cfg_scale: Option<f64>,
// /// The duration of the generated video in seconds
// #[serde(skip_serializing_if = "Option::is_none")]
// pub duration: Option<String>,
// #[serde(skip_serializing_if = "Option::is_none")]
// pub negative_prompt: Option<String>,
// pub prompt: String,
// }
//
// #[derive(Debug, Serialize, Deserialize, Default)]
// pub struct Trajectory {
// /// X coordinate of the motion trajectory
// /// 279
// pub x: i64,
// /// Y coordinate of the motion trajectory
// /// 219
// pub y: i64,
// }
//
// #[derive(Debug, Serialize, Deserialize, Default)]
// pub struct V1ImageToVideoRequest {
// /// The aspect ratio of the generated video frame
// #[serde(skip_serializing_if = "Option::is_none")]
// pub aspect_ratio: Option<String>,
// /// The CFG (Classifier Free Guidance) scale is a measure of how close you want
// /// the model to stick to your prompt.
// #[serde(skip_serializing_if = "Option::is_none")]
// pub cfg_scale: Option<f64>,
// /// The duration of the generated video in seconds
// #[serde(skip_serializing_if = "Option::is_none")]
// pub duration: Option<String>,
// /// List of dynamic masks
// #[serde(skip_serializing_if = "Option::is_none")]
// pub dynamic_masks: Option<Vec<Option<DynamicMask>>>,
// /// URL of the image to be used for the video
// /// "https://h2.inkwai.com/bs2/upload-ylab-stunt/se/ai_portal_queue_mmu_image_upscale_aiweb/3214b798-e1b4-4b00-b7af-72b5b0417420_raw_image_0.jpg"
// pub image_url: String,
// #[serde(skip_serializing_if = "Option::is_none")]
// pub negative_prompt: Option<String>,
// /// The prompt for the video
// /// "The astronaut stood up and walked away"
// pub prompt: String,
// /// URL of the image for Static Brush Application Area (Mask image created by users using the motion brush)
// /// "https://h2.inkwai.com/bs2/upload-ylab-stunt/ai_portal/1732888177/cOLNrShrSO/static_mask.png"
// #[serde(skip_serializing_if = "Option::is_none")]
// pub static_mask_url: Option<String>,
// /// URL of the image to be used for the end of the video
// #[serde(skip_serializing_if = "Option::is_none")]
// pub tail_image_url: Option<String>,
// }
//
// #[derive(Debug, Serialize, Deserialize, Default)]
// pub struct V1TextToVideoRequest {
// /// Advanced Camera control parameters
// #[serde(skip_serializing_if = "Option::is_none")]
// pub advanced_camera_control: Option<Option<CameraControl>>,
// /// The aspect ratio of the generated video frame
// #[serde(skip_serializing_if = "Option::is_none")]
// pub aspect_ratio: Option<String>,
// /// Camera control parameters
// #[serde(skip_serializing_if = "Option::is_none")]
// pub camera_control: Option<String>,
// /// The CFG (Classifier Free Guidance) scale is a measure of how close you want
// /// the model to stick to your prompt.
// #[serde(skip_serializing_if = "Option::is_none")]
// pub cfg_scale: Option<f64>,
// /// The duration of the generated video in seconds
// #[serde(skip_serializing_if = "Option::is_none")]
// pub duration: Option<String>,
// #[serde(skip_serializing_if = "Option::is_none")]
// pub negative_prompt: Option<String>,
// pub prompt: String,
// }
//
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct ValidationError {
pub loc: Vec<serde_json::Value>,
pub msg: String,
#[serde(rename = "type")]
pub ty: String,
}
//
// #[derive(Debug, Serialize, Deserialize, Default)]
// pub struct VideoEffectsOutput {
// /// The generated video
// /// {"content_type":"video/mp4","file_name":"output.mp4","url":"https://storage.googleapis.com/falserverless/kling/kling_ex.mp4.mp4"}
// pub video: File,
// }
//
// #[derive(Debug, Serialize, Deserialize, Default)]
// pub struct VideoEffectsRequest {
// /// The duration of the generated video in seconds
// #[serde(skip_serializing_if = "Option::is_none")]
// pub duration: Option<String>,
// /// The effect scene to use for the video generation
// /// "hug"
// pub effect_scene: String,
// /// URL of the image to be used for the squish and expansion video
// /// "https://h2.inkwai.com/bs2/upload-ylab-stunt/se/ai_portal_queue_mmu_image_upscale_aiweb/3214b798-e1b4-4b00-b7af-72b5b0417420_raw_image_0.jpg"
// pub image_url: String,
// /// URL of images to be used for hug, kiss or heart_gesture video.
// /// ["https://storage.googleapis.com/falserverless/juggernaut_examples/VHXMavzPyI27zi6JseyL4.png","https://storage.googleapis.com/falserverless/juggernaut_examples/QEW5VrzccxGva7mPfEXjf.png"]
// #[serde(skip_serializing_if = "Option::is_none")]
// pub input_image_urls: Option<Vec<Option<String>>>,
// }
/// Kling 1.0
///
/// Category: text-to-video
/// Machine Type: A100
///
///
/// Kling 1.6 (pro) Image to Video API.
pub fn image_to_video(
params: ProImageToVideoRequest,
) -> FalRequest<ProImageToVideoRequest, I2VOutput> {
FalRequest::new("fal-ai/kling-video/v2.1/pro/image-to-video", params)
}
@@ -1,14 +0,0 @@
#[cfg(any(
feature = "endpoints",
feature = "endpoints_fal-ai",
feature = "endpoints_fal-ai_kling-video"
))]
#[cfg_attr(
docsrs,
doc(cfg(any(
feature = "endpoints",
feature = "endpoints_fal-ai",
feature = "endpoints_fal-ai_kling-video"
)))
)]
pub mod image_to_video;
-14
View File
@@ -138,20 +138,6 @@ pub mod birefnet;
)))
)]
pub mod bria;
#[cfg(any(
feature = "endpoints",
feature = "endpoints_fal-ai",
feature = "endpoints_fal-ai_bytedance"
))]
#[cfg_attr(
docsrs,
doc(cfg(any(
feature = "endpoints",
feature = "endpoints_fal-ai",
feature = "endpoints_fal-ai_bytedance"
)))
)]
pub mod bytedance;
#[cfg(any(
feature = "endpoints",
feature = "endpoints_fal-ai",
+2 -5
View File
@@ -22,14 +22,11 @@ pub enum Status {
Completed,
}
// NB(bt, 2025-06-13): Some of these fields have started to become optional.
// eg, errors:
// "missing field `source`", line: 1, column: 1019
#[derive(Debug, Serialize, Deserialize)]
pub struct RequestLog {
pub timestamp: String,
pub level: Option<String>, // NB(bt): Now optional
pub source: Option<String>, // NB(bt): Now optional
pub level: String,
pub source: String,
pub message: String,
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

@@ -43,17 +43,11 @@ const appMenuTabs: MenuIconItem[] = [
id: "2D",
label: "2D Canvas",
icon: <FontAwesomeIcon icon={faPaintbrush} />,
imageSrc: "/resources/gifs/artcraft-canvas-demo.gif",
description: "Easy edits. Great for graphic design.",
large: true,
},
{
id: "3D",
label: "3D Editor",
icon: <FontAwesomeIcon icon={faCube} />,
imageSrc: "/resources/gifs/artcraft-3d-demo.gif",
description: "Precision control. Great for AI film.",
large: true,
},
{
id: "IMAGE",
@@ -21,7 +21,6 @@ export interface NodeProps {
offsetX?: number;
offsetY?: number;
zIndex?: number;
locked?: boolean;
}
export class Node {
@@ -45,7 +44,6 @@ export class Node {
offsetX: number;
offsetY: number;
zIndex: number;
locked: boolean;
constructor({
id,
@@ -67,8 +65,7 @@ export class Node {
scaleY = 1,
offsetX = 0,
offsetY = 0,
zIndex = 0,
locked = false
zIndex = 0
}: NodeProps) {
this.id = id;
this.x = x;
@@ -90,7 +87,6 @@ export class Node {
this.offsetX = offsetX;
this.offsetY = offsetY;
this.zIndex = zIndex;
this.locked = locked;
}
setPosition(x: number, y: number): void {
@@ -1,7 +1,6 @@
import { useState, useRef, useEffect } from "react";
import { PaintSurface } from "./PaintSurface";
// https://github.com/SaladTechnologies/comfyui-api
import "./App.css";
import PromptEditor from "./PromptEditor/PromptEditor";
import SideToolbar from "./components/ui/SideToolbar";
@@ -18,43 +17,6 @@ import Konva from "konva"; // just for types
import { setCanvasRenderBitmap } from "../../signals/canvasRenderBitmap"
import { captureStageImageBitmap } from "./hooks/useUpdateSnapshot"
import { ContextMenuContainer } from "./components/ui/ContextMenu";
import {
FalBackgroundRemoval,
FalBackgroundRemovalRequest,
} from "@storyteller/tauri-api";
/**
* This decodes Base64 encoded PNG images into an image element.
* @param base64String a standard (not "web safe") base64-encoded string
*/
export const DecodeBase64ToImage = async (base64String: string) : Promise<ImageBitmap> => {
// Create an image element
const img = document.createElement("img");
// Convert base64 to data URL if it doesn't include the prefix
const dataUrl = base64String.startsWith("data:")
? base64String
: `data:image/png;base64,${base64String}`;
// Create a promise to handle the image loading
return new Promise((resolve, reject) => {
img.onload = async () => {
try {
const bitmap = await createImageBitmap(img);
resolve(bitmap);
} catch (error) {
reject(error);
}
};
img.onerror = () => reject(new Error("Failed to load image"));
// Set the source to trigger loading
img.src = dataUrl;
});
}
const PageDraw = () => {
//useStateSceneLoader();
@@ -311,6 +273,7 @@ const PageDraw = () => {
<div className="relative z-0">
<ContextMenuContainer
onAction={(e, action) => {
if (action === "contextMenu") {
const hasSelection = store.selectedNodeIds.length > 0;
if (hasSelection) {
@@ -322,40 +285,11 @@ const PageDraw = () => {
return false
}
}
return false
}}
onMenuAction={async (action) => {
switch (action) {
case 'LOCK':
store.toggleLock(store.selectedNodeIds);
break;
case 'REMOVE_BACKGROUND':
// returns a success if we have selected images only
await store.removeBackground(store.selectedNodeIds,
async (success: boolean, image_base64: string, message: string) => {
if (!success) {
console.error(message);
return { success: false };
}
try {
const response = await FalBackgroundRemoval({ base64_image: image_base64 });
if (response.status !== "success" || !("payload" in response)) {
console.error("Failed to remove background", response);
return { success: false };
}
const base64String = response.payload?.base64_bytes as string;
const binaryString = atob(base64String);
const bytes = Uint8Array.from(binaryString, c => c.charCodeAt(0));
const blob = new Blob([bytes], { type: "image/png" });
const file = new File([blob], "generated_image.png", { type: blob.type });
return { success: true, file };
} catch (error) {
console.error("Failed to remove background", error);
return { success: false };
}
});
break;
}}
onMenuAction={(action) => {
switch (action) {
case 'BRING_TO_FRONT':
store.bringToFront(store.selectedNodeIds);
break;
@@ -368,22 +302,10 @@ const PageDraw = () => {
case 'SEND_TO_BACK':
store.sendToBack(store.selectedNodeIds);
break;
case 'DUPLICATE':
store.copySelectedItems()
store.pasteItems()
break;
case 'DELETE':
store.deleteSelectedItems()
break;
default:
// No action needed for unhandled cases
}
}}
isLocked={store.selectedNodeIds.some(id => {
const node = store.nodes.find(n => n.id === id);
const lineNode = store.lineNodes.find(n => n.id === id);
return (node?.locked || lineNode?.locked) ?? false;
})}
>
<PaintSurface
nodes={store.nodes}
@@ -185,7 +185,13 @@ export const PaintSurface = ({
isWithinLeftPanel(stagePoint)
) {
const lineId = `line-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
let opacity = activeTool === "draw" ? store.brushOpacity : 1;
let opacity;
if (activeTool === "draw") {
opacity = store.brushOpacity
} else {
opacity = 1
}
const newLineNode: LineNode = {
id: lineId,
@@ -194,10 +200,10 @@ export const PaintSurface = ({
stroke: activeTool === "draw" ? brushColor : fillColor || "#ffffff",
strokeWidth: brushSize / stage.scaleX(),
draggable: true,
opacity: opacity
opacity: opacity // Add opacity to the line node
};
store.selectNode(null);
store.addLineNode(newLineNode, false); // Don't save state when starting line
store.addLineNode(newLineNode);
setCurrentLineId(lineId);
setIsDrawing(true);
setLastPoint(stagePoint);
@@ -274,6 +280,7 @@ export const PaintSurface = ({
y: (point.y - stage.y()) / stage.scaleY(),
};
// Only add point if it's within the left panel bounds
if (isWithinLeftPanel(stagePoint)) {
const currentLine = store.lineNodes.find(
(line) => line.id === currentLineId,
@@ -284,7 +291,7 @@ export const PaintSurface = ({
stagePoint.x,
stagePoint.y,
];
store.updateLineNode(currentLineId, { points: updatedPoints }, false); // Don't save state while drawing
store.updateLineNode(currentLineId, { points: updatedPoints });
}
setLastPoint(stagePoint);
} else {
@@ -332,10 +339,6 @@ export const PaintSurface = ({
};
const handleStageMouseUp = () => {
if (isDrawing) {
store.saveState(); // Save state only when the stroke is complete
}
if (isSelecting && selectionRect) {
// Calculate the bounds of the selection rectangle
const left = Math.min(selectionRect.startX, selectionRect.endX);
@@ -543,25 +546,6 @@ export const PaintSurface = ({
return;
}
// allow selection
if (e.evt.button === 2) {
const node = store.nodes.find(n => n.id === nodeId);
const lineNode = store.lineNodes.find(n => n.id === nodeId);
const isLocked = (node?.locked || lineNode?.locked) ?? false;
// If locked, only allow selection for context menu
if (isLocked) {
store.selectNode(nodeId);
return;
}
}
// Don't select locked nodes
const node = nodes.find(n => n.id === nodeId) || store.lineNodes.find(n => n.id === nodeId);
if (node?.locked) {
return;
}
// Check if Ctrl/Cmd key is pressed
const isMultiSelect = e.evt.ctrlKey || e.evt.metaKey;
@@ -778,7 +762,7 @@ export const PaintSurface = ({
onMouseLeave={handleNodeMouseLeave}
draggable={draggableIfToolsNotActive(
activeTool,
lineNode.draggable && lineNode.locked == false,
lineNode.draggable,
)}
onDragMove={(e) => handleLineDragMove(e, lineNode.id)}
onDragStart={(e) => handleLineDragStart(e, lineNode.id)}
@@ -815,7 +799,7 @@ export const PaintSurface = ({
offsetX={node.offsetX || 0}
offsetY={node.offsetY || 0}
zIndex={node.zIndex}
draggable={draggableIfToolsNotActive(activeTool, node.draggable && node.locked == false)}
draggable={draggableIfToolsNotActive(activeTool, node.draggable)}
onMouseDown={(e) => handleNodeMouseDown(e, node.id)}
onTap={(e) => handleNodeMouseDown(e, node.id)}
onMouseEnter={handleNodeMouseEnter}
@@ -847,7 +831,7 @@ export const PaintSurface = ({
offsetX={node.offsetX || 0}
offsetY={node.offsetY || 0}
zIndex={node.zIndex}
draggable={draggableIfToolsNotActive(activeTool, node.draggable && node.locked == false)}
draggable={draggableIfToolsNotActive(activeTool, node.draggable)}
onMouseDown={(e) => handleNodeMouseDown(e, node.id)}
onTap={(e) => handleNodeMouseDown(e, node.id)}
onMouseEnter={handleNodeMouseEnter}
@@ -879,7 +863,7 @@ export const PaintSurface = ({
offsetX={node.offsetX || 0}
offsetY={node.offsetY || 0}
zIndex={node.zIndex}
draggable={draggableIfToolsNotActive(activeTool, node.draggable && node.locked == false)}
draggable={draggableIfToolsNotActive(activeTool, node.draggable)}
onMouseDown={(e) => handleNodeMouseDown(e, node.id)}
onTap={(e) => handleNodeMouseDown(e, node.id)}
onMouseEnter={handleNodeMouseEnter}
@@ -927,7 +911,7 @@ export const PaintSurface = ({
scaleY={node.scaleY || 1}
offsetX={node.offsetX || 0}
offsetY={node.offsetY || 0}
draggable={draggableIfToolsNotActive(activeTool, node.draggable && node.locked == false)}
draggable={draggableIfToolsNotActive(activeTool, node.draggable)}
onMouseDown={(e) => handleNodeMouseDown(e, node.id)}
onTap={(e) => handleNodeMouseDown(e, node.id)}
onMouseEnter={handleNodeMouseEnter}
@@ -1,5 +1,4 @@
import React, { useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';
export interface MenuPosition {
x: number;
@@ -34,14 +33,14 @@ export const ContextMenu: React.FC<ContextMenuProps> = ({ items, onAction, onClo
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [onClose]);
return createPortal(
return (
<div
ref={menuRef}
style={{
left: `${position.x}px`,
top: `${position.y}px`,
}}
className="fixed z-[9999999] min-w-[220px] bg-[#1A1A1A] rounded-lg shadow-xl border border-[#333333] py-1 select-none"
className="fixed z-50 min-w-[220px] bg-[#1A1A1A] rounded-lg shadow-xl border border-[#333333] py-1 select-none"
>
{items.map((item, index) => (
<React.Fragment key={index}>
@@ -60,8 +59,7 @@ export const ContextMenu: React.FC<ContextMenuProps> = ({ items, onAction, onClo
{item.divider && <div className="h-[1px] bg-[#333333] mx-2 my-1" />}
</React.Fragment>
))}
</div>,
document.body
</div>
);
};
@@ -69,9 +67,8 @@ interface ContextMenuContainerProps {
children: React.ReactNode;
items?: MenuItem[];
onAction?: (e: React.MouseEvent, actionName: string) => boolean | void;
onMenuAction?: (action: string) => void;
onMenuAction?: (action: string) => void; // New prop for menu item actions
className?: string;
isLocked?: boolean;
}
export const ContextMenuContainer: React.FC<ContextMenuContainerProps> = ({
@@ -79,27 +76,11 @@ export const ContextMenuContainer: React.FC<ContextMenuContainerProps> = ({
items = defaultMenuItems,
onAction,
onMenuAction,
className = '',
isLocked = false
className = ''
}) => {
const [showMenu, setShowMenu] = React.useState(false);
const [position, setPosition] = React.useState<MenuPosition>({ x: 0, y: 0 });
const filteredItems = React.useMemo(() => {
if (isLocked) {
return [
{
icon: <svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M8 11V7a4 4 0 118 0m-4 8v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2z" strokeWidth="2" strokeLinecap="round"/>
</svg>,
label: 'Unlock',
action: 'LOCK'
}
];
}
return items;
}, [isLocked, items]);
const handleContextMenu = (e: React.MouseEvent) => {
e.preventDefault();
const newPosition = { x: e.clientX, y: e.clientY };
@@ -125,12 +106,12 @@ export const ContextMenuContainer: React.FC<ContextMenuContainerProps> = ({
<div
onClick={handleClick}
onContextMenu={handleContextMenu}
className={`w-full h-full relative z-[99999] ${className}`}
className={`w-full h-full ${className}`}
>
{children}
{showMenu && (
<ContextMenu
items={filteredItems}
items={items}
onAction={handleMenuAction}
position={position}
onClose={() => setShowMenu(false)}
@@ -193,24 +174,5 @@ const defaultMenuItems: MenuItem[] = [
label: 'Duplicate',
action: 'DUPLICATE'
},
{
icon: (
<svg
className="w-4 h-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
>
<path
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6M9 7h6m-7 0V4a1 1 0 011-1h4a1 1 0 011 1v3"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
),
label: 'Delete',
action: 'DELETE'
},
];
@@ -18,29 +18,6 @@ export type LineNode = {
offsetX?: number;
offsetY?: number;
zIndex: number;
locked?: boolean; // Add locked property
};
// Logic to remove background from image nodes would go here
const convertFileToBase64 = (file: File): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => {
if (reader.result) {
resolve(reader.result as string);
} else {
reject(new Error("Failed to convert file to base64."));
}
};
reader.onerror = () => {
reject(new Error("Error reading file."));
};
reader.readAsDataURL(file);
});
};
export type ActiveTool = 'select' | 'draw' | 'eraser' | 'backgroundColor' | 'shape';
@@ -89,9 +66,9 @@ interface SceneState {
saveState: () => void;
// Add new actions for line nodes
addLineNode: (lineNode: LineNode, shouldSaveState: boolean) => void;
addLineNode: (lineNode: LineNode) => void;
removeLineNode: (id: string, shouldSaveState?: boolean) => void;
updateLineNode: (id: string, updates: Partial<LineNode>, shouldSaveState: boolean) => void;
updateLineNode: (id: string, updates: Partial<LineNode>) => void;
moveLineNode: (id: string, dx: number, dy: number) => void;
// Add a specific method for file uploads
@@ -133,10 +110,6 @@ interface SceneState {
sendToBack: (nodeIds: string[]) => void;
bringForward: (nodeIds: string[]) => void;
sendBackward: (nodeIds: string[]) => void;
removeBackground: (nodeIds: string[], operation: (success: boolean,base64_image:string, message: string) => Promise<{ success: boolean; file?: File }>) => Promise<void>;
// Add new lock action
toggleLock: (nodeIds: string[]) => void;
}
const generateId = (): string => {
@@ -516,18 +489,17 @@ export const useSceneStore = create<SceneState>((set, get) => ({
},
// Add new line node actions
addLineNode: (lineNode: LineNode, shouldSaveState: boolean = true) => {
addLineNode: (lineNode: LineNode) => {
set((state) => {
const nextZ = getNextZIndex(state.nodes, state.lineNodes);
const newLineNode = {
...lineNode,
zIndex: nextZ
};
return { lineNodes: [...state.lineNodes, newLineNode] };
});
if (shouldSaveState) {
get().saveState();
}
get().saveState();
},
removeLineNode: (id: string, shouldSaveState: boolean = true) => {
@@ -548,7 +520,7 @@ export const useSceneStore = create<SceneState>((set, get) => ({
}
},
updateLineNode: (id: string, updates: Partial<LineNode>, shouldSaveState: boolean = true) => {
updateLineNode: (id: string, updates: Partial<LineNode>) => {
set((state) => {
const newLineNodes = state.lineNodes.map(node => {
if (node.id === id) {
@@ -562,9 +534,7 @@ export const useSceneStore = create<SceneState>((set, get) => ({
});
return { lineNodes: newLineNodes };
});
if (shouldSaveState) {
get().saveState();
}
get().saveState();
},
moveLineNode: (id: string, dx: number, dy: number) => {
@@ -596,8 +566,7 @@ export const useSceneStore = create<SceneState>((set, get) => ({
const img = new Image();
img.onload = () => {
const aspectRatio = img.naturalWidth / img.naturalHeight;
// Increase default size to 512px width while maintaining aspect ratio
const finalWidth = width || Math.min(img.naturalWidth, 512);
const finalWidth = width || Math.min(img.naturalWidth, 300);
const finalHeight = height || finalWidth / aspectRatio;
console.log('Image loaded with dimensions:', {
naturalWidth: img.naturalWidth,
@@ -1162,90 +1131,4 @@ export const useSceneStore = create<SceneState>((set, get) => ({
});
get().saveState();
},
removeBackground: async (nodeIds: string[], operation: (success: boolean, base64_image:string,message: string) => Promise<{ success: boolean; file?: File }>) => {
const hasImageNodes = nodeIds.some(id => {
const node = get().nodes.find(n => n.id === id);
return node ? node.type === 'image': false;
});
let callbackSuccess = false
if (hasImageNodes) {
console.log("Selected nodes include image types.");
const firstNode = get().nodes.find(node => nodeIds.includes(node.id) && node.type === 'image');
if (firstNode) {
const imageFile = firstNode.imageFile; // Assuming the file is stored in the node
if (imageFile !== undefined) {
try {
const base64Image = await convertFileToBase64(imageFile);
console.log("Base64 Image:", base64Image);
// Additional logic to handle the base64 image can be added here
const { success, file } = await operation(true, base64Image, "Removing Background."); // Indicate success in removing background
callbackSuccess = success;
if (callbackSuccess) {
// save the state of the image.
set((state: SceneState) => ({
nodes: state.nodes.map((node: Node) => {
if (node.id === firstNode.id) {
// Create an object URL for immediate preview if needed
console.log("Updated Node!")
const updatedImageUrl: string | undefined = file ? URL.createObjectURL(file) : node.imageUrl;
return new Node({
...node,
imageFile: file,
imageUrl: updatedImageUrl,
});
}
return node;
}),
}));
}
} catch (error) {
console.error("Error converting file to base64:", error);
}
}
console.log("Image File:", imageFile);
} else {
console.warn("No image node found or first node is not an image.");
}
} else {
console.log("No selected nodes are of image type.");
operation(false,"","Did not select a Images"); // Indicate failure to remove background
}
},
// Add toggleLock action
toggleLock: (nodeIds: string[]) => {
set((state) => {
// Update regular nodes
const updatedNodes = state.nodes.map(node => {
if (nodeIds.includes(node.id)) {
return new Node({
...node,
locked: !node.locked
});
}
return node;
});
// Update line nodes
const updatedLineNodes = state.lineNodes.map(node => {
if (nodeIds.includes(node.id)) {
return {
...node,
locked: !node.locked
};
}
return node;
});
return {
nodes: updatedNodes,
lineNodes: updatedLineNodes
};
});
get().saveState();
}
}));
@@ -6,7 +6,7 @@ import { ControlsTopButtons } from "./comps/ControlsTopButtons";
import { ControlPanelSceneObject } from "./comps/ControlPanelSceneObject";
import { PreviewEngineCamera } from "./comps/PreviewEngineCamera";
import { PreviewFrameImage } from "./comps/PreviewFrameImage";
import { authentication, pageHeight, pageWidth } from "~/signals";
import { pageHeight, pageWidth } from "~/signals";
import { PoseModeSelector } from "./comps/PoseModeSelector";
import ImageToVideo from "../PageVideo/ImageToVideo";
import TextToImage from "../PageImage/TextToImage";
@@ -21,7 +21,6 @@ import {
enableHotkeyInput,
gridVisibility,
setGridVisibility,
DomLevels,
} from "~/pages/PageEnigma/signals";
import { EditorCanvas } from "./comps/EngineCanvases";
import { SceneContainer } from "./comps/SceneContainer";
@@ -54,7 +53,7 @@ import {
} from "~/components/signaled/TopBar/TopBar";
import { uploadPlaneFromMediaToken } from "~/components/reusable/UploadModalMedia/uploadPlane";
import { addObject } from "./signals/objectGroup/addObject";
import { AssetType, AUTH_STATUS } from "~/enums";
import { AssetType } from "~/enums";
import { v4 as uuidv4 } from "uuid";
import { MediaItem } from "~/pages/PageEnigma/models";
import { UploaderState } from "~/models";
@@ -72,7 +71,7 @@ import {
// videoGenerationModels,
// useModelSelectorStore,
} from "@storyteller/ui-model-selector";
import { LoginModal } from "@storyteller/ui-login-modal";
import PageDraw from "../PageDraw/PageDraw";
import { useTabStore } from "../Stores/TabState";
@@ -542,7 +541,7 @@ export const PageEditor = () => {
</div>
)}
<LoginModal
{/*<LoginModal
onClose={() => {}}
videoSrc2D="/resources/videos/artcraft-canvas-demo.mp4"
videoSrc3D="/resources/videos/artcraft-3d-demo.mp4"
@@ -558,7 +557,7 @@ export const PageEditor = () => {
authentication.status.value = AUTH_STATUS.LOGGED_IN;
authentication.userInfo.value = userInfo;
}}
/>
/> */}
</div>
);
};
@@ -8,42 +8,19 @@ import {
import { ButtonIconSelect } from "@storyteller/ui-button-icon-select";
import { Button } from "@storyteller/ui-button";
import { Tooltip } from "@storyteller/ui-tooltip";
import {
showActionReminder,
isActionReminderOpen,
} from "@storyteller/ui-action-reminder-modal";
import { SettingsModal } from "@storyteller/ui-settings-modal";
import { EngineContext } from "../../contexts/EngineContext";
import { useContext, useEffect, useState, useRef, useCallback } from "react";
import { useContext, useEffect, useState } from "react";
import { assetModalVisibleDuringDrag, assetModalVisible } from "../../signals";
import {
Create3dModal,
useCreate3dModalStore,
// eslint-disable-next-line import/no-unresolved
} from "@storyteller/ui-create-3d-modal";
// import { v4 as uuidv4 } from "uuid";
// import { addObject } from "../../signals/objectGroup/addObject";
// import { MediaItem } from "../../models/assets";
import { GetFalApiKey } from "@storyteller/tauri-api"; // Fix import path
// eslint-disable-next-line import/no-unresolved
// import { AssetType } from "~/enums";
import { AssetModal } from "../AssetMenu/AssetModal";
import { selectedMode } from "../../signals/selectedMode";
import { useSignals } from "@preact/signals-react/runtime";
import { outlinerState } from "../../signals/outliner/outliner";
import { twMerge } from "tailwind-merge";
// eslint-disable-next-line import/no-unresolved
import { setLogoutStates } from "~/signals/authentication/utilities";
export const Controls3D = () => {
useSignals();
const editorEngine = useContext(EngineContext);
const [showEmptySceneTooltip, setShowEmptySceneTooltip] = useState(false);
// Action reminder is now handled through signals
const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);
// Track processed 3D models by their media token to prevent duplicates
const processedModelsRef = useRef<Record<string, boolean>>({});
useEffect(() => {
// Check if scene is empty and onboarding helper is not visible
@@ -102,85 +79,6 @@ export const Controls3D = () => {
editorEngine.change_mode("scale");
};
// ----- TODO LATER - BFlat for auto add 3d model to scene -----
// Function to add the generated 3D model to the scene
// const addGeneratedModelToScene = useCallback((mediaToken: string) => {
// console.log("[DEBUG] addGeneratedModelToScene called with token:", mediaToken);
// // Create a MediaItem object for the 3D model
// const mediaItem: MediaItem = {
// version: 1,
// type: AssetType.OBJECT,
// media_id: mediaToken,
// name: "3D Model",
// object_uuid: uuidv4(),
// };
// // Add the object to the scene
// console.log("[DEBUG] Calling addObject with:", mediaItem);
// try {
// addObject(mediaItem);
// console.log("[DEBUG] addObject called successfully");
// } catch (error) {
// console.error("[DEBUG] Error in addObject:", error);
// }
// }, []);
const handleOpenModal = () => {
assetModalVisibleDuringDrag.value = true;
assetModalVisible.value = true;
};
const handleOpenCreate3dModal = async () => {
try {
const falApiKeyResult = await GetFalApiKey();
if (
falApiKeyResult.status === "success" &&
"payload" in falApiKeyResult &&
falApiKeyResult.payload?.key
) {
// API key exists, open the create 3D modal
useCreate3dModalStore.getState().open();
} else {
// No API key, show the action reminder modal
showActionReminder({
reminderType: "default",
title: "FAL API Key Required",
message:
"To generate 3D models, you need to add a valid FAL API key in your settings.",
primaryActionText: "Add now",
secondaryActionText: "Cancel",
onPrimaryAction: () => {
// Close the action reminder modal first
isActionReminderOpen.value = false;
// Then open the settings modal
openSettingsModal();
},
});
}
} catch (error) {
console.error("Error checking FAL API key:", error);
}
};
const openSettingsModal = () => {
setIsSettingsModalOpen(true);
};
// Handle completed models from the Create3dModal component
const handleModelComplete = useCallback((mediaToken: string) => {
// Check if we've already processed this model
if (mediaToken && !processedModelsRef.current[mediaToken]) {
// Mark this model as processed to avoid duplicates
processedModelsRef.current[mediaToken] = true;
// Add the generated 3D model to the scene
// addGeneratedModelToScene(mediaToken);
}
}, []);
const modes = [
{
value: "move",
@@ -202,6 +100,11 @@ export const Controls3D = () => {
},
];
const handleOpenModal = () => {
assetModalVisibleDuringDrag.value = true;
assetModalVisible.value = true;
};
return (
<>
<div className="flex justify-center">
@@ -245,9 +148,10 @@ export const Controls3D = () => {
>
<Button
icon={faMagicWandSparkles}
className="text-md h-9 w-9 rounded-[10px] bg-white/15 transition-colors hover:bg-white/25"
className="text-md h-9 w-9 rounded-[10px] bg-white/10"
variant="secondary"
onClick={handleOpenCreate3dModal}
disabled={true}
onClick={handleOpenModal}
/>
</Tooltip>
</div>
@@ -263,15 +167,6 @@ export const Controls3D = () => {
</div>
<AssetModal />
<Create3dModal onModelComplete={handleModelComplete} />
<SettingsModal
isOpen={isSettingsModalOpen}
onClose={() => setIsSettingsModalOpen(false)}
globalAccountLogoutCallback={() => setLogoutStates()}
initialSection="accounts"
/>
</>
);
};
+14 -14
View File
@@ -28,6 +28,9 @@
"noEmit": true
},
"references": [
{
"path": "../../libs/components/model-selector"
},
{
"path": "../../libs/components/pagination"
},
@@ -46,15 +49,9 @@
{
"path": "../../libs/components/loading-spinner"
},
{
"path": "../../libs/components/create-3d-modal"
},
{
"path": "../../libs/components/button-icon-select"
},
{
"path": "../../libs/components/slider-v2"
},
{
"path": "../../libs/components/tab-selector"
},
@@ -64,30 +61,33 @@
{
"path": "../../libs/components/button-dropdown"
},
{
"path": "../../libs/tauri-api"
},
{
"path": "../../libs/components/action-reminder-modal"
},
{
"path": "../../libs/components/toaster"
},
{
"path": "../../libs/components/model-selector"
"path": "../../libs/components/login-modal"
},
{
"path": "../../libs/soundboard"
},
{
"path": "../../libs/components/promptbox"
"path": "../../libs/components/slider-v2"
},
{
"path": "../../libs/components/toaster"
},
{
"path": "../../libs/components/popover"
},
{
"path": "../../libs/components/promptbox"
},
{
"path": "../../libs/common"
},
{
"path": "../../libs/tauri-api"
},
{
"path": "../../libs/components/menu-icon-selector"
},

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