diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image/handle_image_artcraft.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image/handle_image_artcraft.rs index 3594e5117c..23faee7425 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/image/handle_image_artcraft.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image/handle_image_artcraft.rs @@ -72,6 +72,8 @@ pub async fn handle_image_artcraft( let request = GenerateFluxPro11UltraTextToImageRequest { uuid_idempotency_token, prompt: request.prompt, + aspect_ratio: None, + num_images: None, }; let result = generate_flux_pro_11_ultra_text_to_image( &ApiHost::Storyteller, diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/video/handle_video_artcraft.rs b/crates/desktop/artcraft/src/core/commands/enqueue/video/handle_video_artcraft.rs index c3a3ec4de9..cee1a5273e 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/video/handle_video_artcraft.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/video/handle_video_artcraft.rs @@ -77,6 +77,7 @@ pub async fn handle_video_artcraft( media_file_token: request.image_media_token, aspect_ratio: Some(GenerateKling16ProAspectRatio::WideSixteenNine), prompt: None, + duration: None, }; let result = generate_kling_16_pro_image_to_video( &ApiHost::Storyteller, diff --git a/crates/lib/api_clients/fal_client/src/requests/webhook/image/enqueue_flux_pro_11_ultra_text_to_image_webhook.rs b/crates/lib/api_clients/fal_client/src/requests/webhook/image/enqueue_flux_pro_11_ultra_text_to_image_webhook.rs index 51946abe20..d0aaba2f76 100644 --- a/crates/lib/api_clients/fal_client/src/requests/webhook/image/enqueue_flux_pro_11_ultra_text_to_image_webhook.rs +++ b/crates/lib/api_clients/fal_client/src/requests/webhook/image/enqueue_flux_pro_11_ultra_text_to_image_webhook.rs @@ -1,7 +1,7 @@ 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::endpoints::fal_ai::flux_pro::v1_1_ultra::{v1_1_ultra, AspectRatioProperty, FluxProUltraTextToImageInput}; use fal::webhook::WebhookResponse; use reqwest::IntoUrl; @@ -9,17 +9,52 @@ pub struct FluxPro11UltraArgs<'a, U: IntoUrl> { pub prompt: &'a str, pub webhook_url: U, pub api_key: &'a FalApiKey, + pub aspect_ratio: FluxPro11UltraAspectRatio, + pub num_images: FluxPro11UltraNumImages, +} + +#[derive(Copy, Clone, Debug)] +pub enum FluxPro11UltraAspectRatio { + Square, // 1:1 + LandscapeFourByThree, // 4:3 + LandscapeSixteenByNine, // 16:9 + PortraitThreeByFour, // 3:4 + PortraitNineBySixteen, // 9:16 + //Custom { width: u32, height: u32 }, // TODO +} + +#[derive(Copy, Clone, Debug)] +pub enum FluxPro11UltraNumImages { + One, // Default + Two, + Three, + Four, } pub async fn enqueue_flux_pro_11_ultra_text_to_image_webhook( args: FluxPro11UltraArgs<'_, U> ) -> Result { + let num_images = match args.num_images { + FluxPro11UltraNumImages::One => 1, + FluxPro11UltraNumImages::Two => 2, + FluxPro11UltraNumImages::Three => 3, + FluxPro11UltraNumImages::Four => 4, + }; + + let aspect_ratio = match args.aspect_ratio { + FluxPro11UltraAspectRatio::Square => AspectRatioProperty::Property_1_1, + FluxPro11UltraAspectRatio::LandscapeFourByThree => AspectRatioProperty::Property_4_3, + FluxPro11UltraAspectRatio::LandscapeSixteenByNine => AspectRatioProperty::Property_16_9, + FluxPro11UltraAspectRatio::PortraitThreeByFour => AspectRatioProperty::Property_3_4, + FluxPro11UltraAspectRatio::PortraitNineBySixteen => AspectRatioProperty::Property_9_16, + }; + let request = FluxProUltraTextToImageInput { prompt: args.prompt.to_string(), + num_images: Some(num_images), + aspect_ratio: Some(aspect_ratio), // Maybe expose - 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 diff --git a/crates/schema/public/api/artcraft_api_defs/src/generate/image/generate_flux_1_dev_text_to_image.rs b/crates/schema/public/api/artcraft_api_defs/src/generate/image/generate_flux_1_dev_text_to_image.rs new file mode 100644 index 0000000000..421bd5e657 --- /dev/null +++ b/crates/schema/public/api/artcraft_api_defs/src/generate/image/generate_flux_1_dev_text_to_image.rs @@ -0,0 +1,49 @@ +use serde_derive::{Deserialize, Serialize}; +use tokens::tokens::generic_inference_jobs::InferenceJobToken; +use utoipa::ToSchema; + +pub const GENERATE_FLUX_1_DEV_TEXT_TO_IMAGE_PATH: &str = "/v1/generate/image/flux_1_dev_text_to_image"; + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct GenerateFlux1DevTextToImageRequest { + /// Idempotency token to prevent duplicate requests. + pub uuid_idempotency_token: String, + + /// Text prompt to generate the image from. + pub prompt: Option, + + /// Aspect ratio of the output images. + pub aspect_ratio: Option, + + /// Number of images to generate. Default is one. + pub num_images: Option, +} + + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum GenerateFlux1DevTextToImageAspectRatio { + Square, // 1:1 + SquareHd, // 1:1 + LandscapeFourByThree, // 4:3 + LandscapeSixteenByNine, // 16:9 + PortraitThreeByFour, // 3:4 + PortraitNineBySixteen, // 9:16 + //Custom { width: u32, height: u32 }, // TODO +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum GenerateFlux1DevTextToImageNumImages { + One, // Default + Two, + Three, + Four, +} + + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct GenerateFlux1DevTextToImageResponse { + pub success: bool, + pub inference_job_token: InferenceJobToken, +} diff --git a/crates/schema/public/api/artcraft_api_defs/src/generate/image/generate_flux_1_schnell_text_to_image.rs b/crates/schema/public/api/artcraft_api_defs/src/generate/image/generate_flux_1_schnell_text_to_image.rs new file mode 100644 index 0000000000..a2d636fdf0 --- /dev/null +++ b/crates/schema/public/api/artcraft_api_defs/src/generate/image/generate_flux_1_schnell_text_to_image.rs @@ -0,0 +1,49 @@ +use serde_derive::{Deserialize, Serialize}; +use tokens::tokens::generic_inference_jobs::InferenceJobToken; +use utoipa::ToSchema; + +pub const GENERATE_FLUX_1_SCHNELL_TEXT_TO_IMAGE_PATH: &str = "/v1/generate/image/flux_1_schnell_text_to_image"; + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct GenerateFlux1SchnellTextToImageRequest { + /// Idempotency token to prevent duplicate requests. + pub uuid_idempotency_token: String, + + /// Text prompt to generate the image from. + pub prompt: Option, + + /// Aspect ratio of the output images. + pub aspect_ratio: Option, + + /// Number of images to generate. Default is one. + pub num_images: Option, +} + + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum GenerateFlux1SchnellTextToImageAspectRatio { + Square, // 1:1 + SquareHd, // 1:1 + LandscapeFourByThree, // 4:3 + LandscapeSixteenByNine, // 16:9 + PortraitThreeByFour, // 3:4 + PortraitNineBySixteen, // 9:16 + //Custom { width: u32, height: u32 }, // TODO +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum GenerateFlux1SchnellTextToImageNumImages { + One, // Default + Two, + Three, + Four, +} + + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct GenerateFlux1SchnellTextToImageResponse { + pub success: bool, + pub inference_job_token: InferenceJobToken, +} diff --git a/crates/schema/public/api/artcraft_api_defs/src/generate/image/generate_flux_pro_11_text_to_image.rs b/crates/schema/public/api/artcraft_api_defs/src/generate/image/generate_flux_pro_11_text_to_image.rs new file mode 100644 index 0000000000..ed7bd6127b --- /dev/null +++ b/crates/schema/public/api/artcraft_api_defs/src/generate/image/generate_flux_pro_11_text_to_image.rs @@ -0,0 +1,46 @@ +use serde_derive::{Deserialize, Serialize}; +use tokens::tokens::generic_inference_jobs::InferenceJobToken; +use utoipa::ToSchema; + +pub const GENERATE_FLUX_PRO_11_TEXT_TO_IMAGE_PATH: &str = "/v1/generate/image/flux_pro_1.1_text_to_image"; + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct GenerateFluxPro11TextToImageRequest { + /// Idempotency token to prevent duplicate requests. + pub uuid_idempotency_token: String, + + /// Text prompt to generate the image from. + pub prompt: Option, + + /// Aspect ratio of the output images. + pub aspect_ratio: Option, + + /// Number of images to generate. Default is one. + pub num_images: Option, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum GenerateFluxPro11TextToImageAspectRatio { + Square, // 1:1 + LandscapeFourByThree, // 4:3 + LandscapeSixteenByNine, // 16:9 + PortraitThreeByFour, // 3:4 + PortraitNineBySixteen, // 9:16 + //Custom { width: u32, height: u32 }, // TODO +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum GenerateFluxPro11TextToImageNumImages { + One, // Default + Two, + Three, + Four, +} + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct GenerateFluxPro11TextToImageResponse { + pub success: bool, + pub inference_job_token: InferenceJobToken, +} diff --git a/crates/schema/public/api/artcraft_api_defs/src/generate/image/generate_flux_pro_11_ultra_text_to_image.rs b/crates/schema/public/api/artcraft_api_defs/src/generate/image/generate_flux_pro_11_ultra_text_to_image.rs index e1a2a7de8d..91292b7a39 100644 --- a/crates/schema/public/api/artcraft_api_defs/src/generate/image/generate_flux_pro_11_ultra_text_to_image.rs +++ b/crates/schema/public/api/artcraft_api_defs/src/generate/image/generate_flux_pro_11_ultra_text_to_image.rs @@ -11,6 +11,32 @@ pub struct GenerateFluxPro11UltraTextToImageRequest { /// Text prompt to generate the image from. pub prompt: Option, + + /// Aspect ratio of the output images. + pub aspect_ratio: Option, + + /// Number of images to generate. Default is one. + pub num_images: Option, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum GenerateFluxPro11UltraTextToImageAspectRatio { + Square, // 1:1 + LandscapeFourByThree, // 4:3 + LandscapeSixteenByNine, // 16:9 + PortraitThreeByFour, // 3:4 + PortraitNineBySixteen, // 9:16 + //Custom { width: u32, height: u32 }, // TODO +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum GenerateFluxPro11UltraTextToImageNumImages { + One, // Default + Two, + Three, + Four, } #[derive(Serialize, Deserialize, ToSchema)] diff --git a/crates/schema/public/api/artcraft_api_defs/src/generate/image/mod.rs b/crates/schema/public/api/artcraft_api_defs/src/generate/image/mod.rs index ed74641a70..b5227b907c 100644 --- a/crates/schema/public/api/artcraft_api_defs/src/generate/image/mod.rs +++ b/crates/schema/public/api/artcraft_api_defs/src/generate/image/mod.rs @@ -1,2 +1,5 @@ +pub mod generate_flux_1_dev_text_to_image; +pub mod generate_flux_1_schnell_text_to_image; +pub mod generate_flux_pro_11_text_to_image; pub mod generate_flux_pro_11_ultra_text_to_image; pub mod remove_image_background; \ No newline at end of file diff --git a/crates/schema/public/api/artcraft_api_defs/src/generate/object/generate_hunyuan_21_image_to_3d.rs b/crates/schema/public/api/artcraft_api_defs/src/generate/object/generate_hunyuan_21_image_to_3d.rs new file mode 100644 index 0000000000..7983a54422 --- /dev/null +++ b/crates/schema/public/api/artcraft_api_defs/src/generate/object/generate_hunyuan_21_image_to_3d.rs @@ -0,0 +1,22 @@ +use serde_derive::{Deserialize, Serialize}; +use tokens::tokens::generic_inference_jobs::InferenceJobToken; +use tokens::tokens::media_files::MediaFileToken; +use utoipa::ToSchema; + +pub const GENERATE_HUNYUAN_2_1_IMAGE_TO_3D_URL_PATH: &str = "/v1/generate/object/hunyuan_2.1_image_to_3d"; + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct GenerateHunyuan21ImageTo3dRequest { + /// Idempotency token to prevent duplicate requests. + pub uuid_idempotency_token: String, + + /// Source of the file to convert to 3D + /// It must be an image. + pub media_file_token: Option, +} + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct GenerateHunyuan21ImageTo3dResponse { + pub success: bool, + pub inference_job_token: InferenceJobToken, +} diff --git a/crates/schema/public/api/artcraft_api_defs/src/generate/object/mod.rs b/crates/schema/public/api/artcraft_api_defs/src/generate/object/mod.rs index 4f412d5a9c..0ef172ab4e 100644 --- a/crates/schema/public/api/artcraft_api_defs/src/generate/object/mod.rs +++ b/crates/schema/public/api/artcraft_api_defs/src/generate/object/mod.rs @@ -1 +1,2 @@ +pub mod generate_hunyuan_21_image_to_3d; pub mod generate_hunyuan_2_image_to_3d; \ No newline at end of file diff --git a/crates/schema/public/api/artcraft_api_defs/src/generate/video/generate_kling_1_6_pro_image_to_video.rs b/crates/schema/public/api/artcraft_api_defs/src/generate/video/generate_kling_1_6_pro_image_to_video.rs index 2a03c28862..9807530180 100644 --- a/crates/schema/public/api/artcraft_api_defs/src/generate/video/generate_kling_1_6_pro_image_to_video.rs +++ b/crates/schema/public/api/artcraft_api_defs/src/generate/video/generate_kling_1_6_pro_image_to_video.rs @@ -17,11 +17,15 @@ pub struct GenerateKling16ProImageToVideoRequest { /// Optional text prompt. pub prompt: Option, - /// Optional: apsect ratio of the generated video. + /// Optional: aspect ratio of the generated video. pub aspect_ratio: Option, + + /// Optional: aspect ratio of the generated video. + pub duration: Option, } #[derive(Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] pub enum GenerateKling16ProAspectRatio { /// 16:9 aspect ratio WideSixteenNine, @@ -31,6 +35,13 @@ pub enum GenerateKling16ProAspectRatio { Square, } +#[derive(Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum GenerateKling16ProDuration { + FiveSeconds, + TenSeconds, +} + #[derive(Serialize, Deserialize, ToSchema)] pub struct GenerateKling16ProImageToVideoResponse { pub success: bool, diff --git a/crates/schema/public/api/artcraft_api_defs/src/generate/video/generate_kling_2_1_master_image_to_video.rs b/crates/schema/public/api/artcraft_api_defs/src/generate/video/generate_kling_2_1_master_image_to_video.rs new file mode 100644 index 0000000000..7cdeed6718 --- /dev/null +++ b/crates/schema/public/api/artcraft_api_defs/src/generate/video/generate_kling_2_1_master_image_to_video.rs @@ -0,0 +1,49 @@ +use serde::{Deserialize, Serialize}; +use tokens::tokens::generic_inference_jobs::InferenceJobToken; +use tokens::tokens::media_files::MediaFileToken; +use utoipa::ToSchema; + +pub const GENERATE_KLING_2_1_MASTER_IMAGE_TO_VIDEO_URL_PATH: &str = "/v1/generate/video/kling_2.1_master_image_to_video"; + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct GenerateKling21MasterImageToVideoRequest { + /// Idempotency token to prevent duplicate requests. + pub uuid_idempotency_token: String, + + /// Source of the image file to convert to video. + /// It must be an image. + pub media_file_token: Option, + + /// Optional text prompt. + pub prompt: Option, + + /// Optional: aspect ratio of the generated video. + pub aspect_ratio: Option, + + /// Optional: aspect ratio of the generated video. + pub duration: Option, +} + +#[derive(Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum GenerateKling21MasterAspectRatio { + /// 16:9 aspect ratio + WideSixteenNine, + /// 9:16 aspect ratio + TallNineSixteen, + /// 1:1 aspect ratio + Square, +} + +#[derive(Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum GenerateKling21MasterDuration { + FiveSeconds, + TenSeconds, +} + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct GenerateKling21MasterImageToVideoResponse { + pub success: bool, + pub inference_job_token: InferenceJobToken, +} diff --git a/crates/schema/public/api/artcraft_api_defs/src/generate/video/generate_kling_2_1_pro_image_to_video.rs b/crates/schema/public/api/artcraft_api_defs/src/generate/video/generate_kling_2_1_pro_image_to_video.rs new file mode 100644 index 0000000000..8faf3850da --- /dev/null +++ b/crates/schema/public/api/artcraft_api_defs/src/generate/video/generate_kling_2_1_pro_image_to_video.rs @@ -0,0 +1,49 @@ +use serde::{Deserialize, Serialize}; +use tokens::tokens::generic_inference_jobs::InferenceJobToken; +use tokens::tokens::media_files::MediaFileToken; +use utoipa::ToSchema; + +pub const GENERATE_KLING_2_1_PRO_IMAGE_TO_VIDEO_URL_PATH: &str = "/v1/generate/video/kling_2.1_pro_image_to_video"; + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct GenerateKling21ProImageToVideoRequest { + /// Idempotency token to prevent duplicate requests. + pub uuid_idempotency_token: String, + + /// Source of the image file to convert to video. + /// It must be an image. + pub media_file_token: Option, + + /// Optional text prompt. + pub prompt: Option, + + /// Optional: aspect ratio of the generated video. + pub aspect_ratio: Option, + + /// Optional: aspect ratio of the generated video. + pub duration: Option, +} + +#[derive(Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum GenerateKling21ProAspectRatio { + /// 16:9 aspect ratio + WideSixteenNine, + /// 9:16 aspect ratio + TallNineSixteen, + /// 1:1 aspect ratio + Square, +} + +#[derive(Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum GenerateKling21ProDuration { + FiveSeconds, + TenSeconds, +} + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct GenerateKling21ProImageToVideoResponse { + pub success: bool, + pub inference_job_token: InferenceJobToken, +} diff --git a/crates/schema/public/api/artcraft_api_defs/src/generate/video/generate_seedance_1_0_lite_image_to_video.rs b/crates/schema/public/api/artcraft_api_defs/src/generate/video/generate_seedance_1_0_lite_image_to_video.rs new file mode 100644 index 0000000000..a0464bf8cf --- /dev/null +++ b/crates/schema/public/api/artcraft_api_defs/src/generate/video/generate_seedance_1_0_lite_image_to_video.rs @@ -0,0 +1,45 @@ +use serde::{Deserialize, Serialize}; +use tokens::tokens::generic_inference_jobs::InferenceJobToken; +use tokens::tokens::media_files::MediaFileToken; +use utoipa::ToSchema; + +pub const GENERATE_SEEDANCE_1_0_LITE_IMAGE_TO_VIDEO_URL_PATH: &str = "/v1/generate/video/seedance_1.0_lite_image_to_video"; + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct GenerateSeedance10LiteImageToVideoRequest { + /// Idempotency token to prevent duplicate requests. + pub uuid_idempotency_token: String, + + /// Source of the image file to convert to video. + /// It must be an image. + pub media_file_token: Option, + + /// Optional text prompt. + pub prompt: Option, + + /// Optional: aspect ratio of the generated video. + pub resolution: Option, + + /// Optional: aspect ratio of the generated video. + pub duration: Option, +} + +#[derive(Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum GenerateSeedance10LiteResolution { + FourEightyP, + SevenTwentyP, +} + +#[derive(Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum GenerateSeedance10LiteDuration { + FiveSeconds, + TenSeconds, +} + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct GenerateSeedance10LiteImageToVideoResponse { + pub success: bool, + pub inference_job_token: InferenceJobToken, +} diff --git a/crates/schema/public/api/artcraft_api_defs/src/generate/video/generate_veo_2_image_to_video.rs b/crates/schema/public/api/artcraft_api_defs/src/generate/video/generate_veo_2_image_to_video.rs new file mode 100644 index 0000000000..2ce36271e7 --- /dev/null +++ b/crates/schema/public/api/artcraft_api_defs/src/generate/video/generate_veo_2_image_to_video.rs @@ -0,0 +1,49 @@ +use serde::{Deserialize, Serialize}; +use tokens::tokens::generic_inference_jobs::InferenceJobToken; +use tokens::tokens::media_files::MediaFileToken; +use utoipa::ToSchema; + +pub const GENERATE_VEO_2_IMAGE_TO_VIDEO_URL_PATH: &str = "/v1/generate/video/veo_2_image_to_video"; + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct GenerateVeo2ImageToVideoRequest { + /// Idempotency token to prevent duplicate requests. + pub uuid_idempotency_token: String, + + /// Source of the image file to convert to video. + /// It must be an image. + pub media_file_token: Option, + + /// Optional text prompt. + pub prompt: Option, + + /// Optional: aspect ratio of the generated video. + pub aspect_ratio: Option, + + /// Optional: aspect ratio of the generated video. + pub duration: Option, +} + +#[derive(Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum GenerateVeo2AspectRatio { + Auto, + AutoPreferPortrait, + WideSixteenNine, // 16:9 + TallNineSixteen, // 9:16 +} + +#[derive(Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum GenerateVeo2Duration { + FiveSeconds, + SixSeconds, + SevenSeconds, + EightSeconds, +} + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct GenerateVeo2ImageToVideoResponse { + pub success: bool, + pub inference_job_token: InferenceJobToken, +} diff --git a/crates/schema/public/api/artcraft_api_defs/src/generate/video/mod.rs b/crates/schema/public/api/artcraft_api_defs/src/generate/video/mod.rs index 5a1956a8f5..9dae98d730 100644 --- a/crates/schema/public/api/artcraft_api_defs/src/generate/video/mod.rs +++ b/crates/schema/public/api/artcraft_api_defs/src/generate/video/mod.rs @@ -1 +1,5 @@ -pub mod generate_kling_1_6_pro_image_to_video; \ No newline at end of file +pub mod generate_kling_1_6_pro_image_to_video; +pub mod generate_kling_2_1_master_image_to_video; +pub mod generate_kling_2_1_pro_image_to_video; +pub mod generate_seedance_1_0_lite_image_to_video; +pub mod generate_veo_2_image_to_video; \ No newline at end of file diff --git a/crates/service/web/storyteller_web/src/docs/api_doc.rs b/crates/service/web/storyteller_web/src/docs/api_doc.rs index 33d6790827..316efd69c8 100644 --- a/crates/service/web/storyteller_web/src/docs/api_doc.rs +++ b/crates/service/web/storyteller_web/src/docs/api_doc.rs @@ -1,5 +1,47 @@ use utoipa::OpenApi; +use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageAspectRatio; +use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageNumImages; +use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageRequest; +use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageResponse; +use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageAspectRatio; +use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageNumImages; +use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageRequest; +use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageResponse; +use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageAspectRatio; +use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageNumImages; +use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageRequest; +use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageResponse; +use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageAspectRatio; +use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageNumImages; +use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageRequest; +use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageResponse; +use artcraft_api_defs::generate::image::remove_image_background::RemoveImageBackgroundRequest; +use artcraft_api_defs::generate::image::remove_image_background::RemoveImageBackgroundResponse; +use artcraft_api_defs::generate::object::generate_hunyuan_21_image_to_3d::GenerateHunyuan21ImageTo3dRequest; +use artcraft_api_defs::generate::object::generate_hunyuan_21_image_to_3d::GenerateHunyuan21ImageTo3dResponse; +use artcraft_api_defs::generate::object::generate_hunyuan_2_image_to_3d::GenerateHunyuan2ImageTo3dRequest; +use artcraft_api_defs::generate::object::generate_hunyuan_2_image_to_3d::GenerateHunyuan2ImageTo3dResponse; +use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProAspectRatio; +use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProDuration; +use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProImageToVideoRequest; +use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProImageToVideoResponse; +use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::GenerateKling21MasterAspectRatio; +use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::GenerateKling21MasterDuration; +use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::GenerateKling21MasterImageToVideoRequest; +use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::GenerateKling21MasterImageToVideoResponse; +use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::GenerateKling21ProAspectRatio; +use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::GenerateKling21ProDuration; +use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::GenerateKling21ProImageToVideoRequest; +use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::GenerateKling21ProImageToVideoResponse; +use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteDuration; +use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteImageToVideoRequest; +use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteImageToVideoResponse; +use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteResolution; +use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2AspectRatio; +use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2Duration; +use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2ImageToVideoRequest; +use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2ImageToVideoResponse; use billing_component::stripe::http_endpoints::checkout::create::stripe_create_checkout_session_error::CreateCheckoutSessionError; use billing_component::stripe::http_endpoints::checkout::create::stripe_create_checkout_session_json_handler::*; use billing_component::users::http_endpoints::list_active_user_subscriptions_handler::*; @@ -187,6 +229,18 @@ use crate::http_server::web_utils::response_success_helpers::*; crate::http_server::endpoints::featured_items::create_featured_item_handler::create_featured_item_handler, crate::http_server::endpoints::featured_items::delete_featured_item_handler::delete_featured_item_handler, crate::http_server::endpoints::featured_items::get_is_featured_item_handler::get_is_featured_item_handler, + crate::http_server::endpoints::generate::image::generate_flux_1_dev_text_to_image_handler::generate_flux_1_dev_text_to_image_handler, + crate::http_server::endpoints::generate::image::generate_flux_1_schnell_text_to_image_handler::generate_flux_1_schnell_text_to_image_handler, + crate::http_server::endpoints::generate::image::generate_flux_pro_11_text_to_image_handler::generate_flux_pro_11_text_to_image_handler, + crate::http_server::endpoints::generate::image::generate_flux_pro_11_ultra_text_to_image_handler::generate_flux_pro_11_ultra_text_to_image_handler, + crate::http_server::endpoints::generate::image::remove_image_background_handler::remove_image_background_handler, + crate::http_server::endpoints::generate::object::generate_hunyuan_21_image_to_3d_handler::generate_hunyuan_21_image_to_3d_handler, + crate::http_server::endpoints::generate::object::generate_hunyuan_2_image_to_3d_handler::generate_hunyuan_2_image_to_3d_handler, + crate::http_server::endpoints::generate::video::generate_kling_1_6_pro_video_handler::generate_kling_1_6_pro_video_handler, + crate::http_server::endpoints::generate::video::generate_kling_2_1_master_video_handler::generate_kling_2_1_master_video_handler, + crate::http_server::endpoints::generate::video::generate_kling_2_1_pro_video_handler::generate_kling_2_1_pro_video_handler, + crate::http_server::endpoints::generate::video::generate_seedance_1_0_lite_image_to_video_handler::generate_seedance_1_0_lite_image_to_video_handler, + crate::http_server::endpoints::generate::video::generate_veo_2_image_to_video_handler::generate_veo_2_image_to_video_handler, crate::http_server::endpoints::image_studio::prompt::enqueue_studio_image_generation_handler::enqueue_studio_image_generation_handler, crate::http_server::endpoints::image_studio::upload::upload_snapshot_media_file_handler::upload_snapshot_media_file_handler, crate::http_server::endpoints::inference_job::delete::dismiss_finished_session_jobs_handler::dismiss_finished_session_jobs_handler, @@ -340,9 +394,6 @@ use crate::http_server::web_utils::response_success_helpers::*; AppStateServerInfo, AppStateStatusAlertCategory, AppStateStatusAlertInfo, - EnqueueStudioImageGenRequest, - EnqueueImageGenRequestSuccessResponse, - EnqueueImageGenRequestError, AppStateSubscriptionProductKey, AppStateUserInfo, AppStateUserLocale, @@ -354,8 +405,6 @@ use crate::http_server::web_utils::response_success_helpers::*; BatchGetMediaFilesQueryParams, BatchGetMediaFilesSuccessResponse, BatchGetUserBookmarksError, - UploadSnapshotMediaFileForm, - UploadSnapshotMediaFileSuccessResponse, BatchGetUserBookmarksQueryParams, BatchGetUserBookmarksResponse, BatchGetUserRatingError, @@ -429,9 +478,6 @@ use crate::http_server::web_utils::response_success_helpers::*; EditUsernameResponse, EnqueueFaceFusionCropDimensions, EnqueueFaceFusionWorkflowError, - EnqueueStudioGen2Request, - EnqueueStudioGen2Response, - EnqueueStudioGen2Error, EnqueueFaceFusionWorkflowRequest, EnqueueFaceFusionWorkflowSuccessResponse, EnqueueFbxToGltfRequest, @@ -440,10 +486,16 @@ use crate::http_server::web_utils::response_success_helpers::*; EnqueueGptSovitsModelDownloadError, EnqueueGptSovitsModelDownloadRequest, EnqueueGptSovitsModelDownloadSuccessResponse, + EnqueueImageGenRequestError, + EnqueueImageGenRequestSuccessResponse, EnqueueLivePortraitCropDimensions, EnqueueLivePortraitWorkflowError, EnqueueLivePortraitWorkflowRequest, EnqueueLivePortraitWorkflowSuccessResponse, + EnqueueStudioGen2Error, + EnqueueStudioGen2Request, + EnqueueStudioGen2Response, + EnqueueStudioImageGenRequest, EnqueueTTSRequest, EnqueueTTSRequestError, EnqueueTTSRequestSuccessResponse, @@ -457,6 +509,46 @@ use crate::http_server::web_utils::response_success_helpers::*; FeaturedMediaFile, FeaturedModelWeightForList, FundamentalFrequencyMethod, + GenerateFlux1DevTextToImageAspectRatio, + GenerateFlux1DevTextToImageNumImages, + GenerateFlux1DevTextToImageRequest, + GenerateFlux1DevTextToImageResponse, + GenerateFlux1SchnellTextToImageAspectRatio, + GenerateFlux1SchnellTextToImageNumImages, + GenerateFlux1SchnellTextToImageRequest, + GenerateFlux1SchnellTextToImageResponse, + GenerateFluxPro11TextToImageAspectRatio, + GenerateFluxPro11TextToImageNumImages, + GenerateFluxPro11TextToImageRequest, + GenerateFluxPro11TextToImageResponse, + GenerateFluxPro11UltraTextToImageAspectRatio, + GenerateFluxPro11UltraTextToImageNumImages, + GenerateFluxPro11UltraTextToImageRequest, + GenerateFluxPro11UltraTextToImageResponse, + GenerateHunyuan21ImageTo3dRequest, + GenerateHunyuan21ImageTo3dResponse, + GenerateHunyuan2ImageTo3dRequest, + GenerateHunyuan2ImageTo3dResponse, + GenerateKling16ProAspectRatio, + GenerateKling16ProDuration, + GenerateKling16ProImageToVideoRequest, + GenerateKling16ProImageToVideoResponse, + GenerateKling21MasterAspectRatio, + GenerateKling21MasterDuration, + GenerateKling21MasterImageToVideoRequest, + GenerateKling21MasterImageToVideoResponse, + GenerateKling21ProAspectRatio, + GenerateKling21ProDuration, + GenerateKling21ProImageToVideoRequest, + GenerateKling21ProImageToVideoResponse, + GenerateSeedance10LiteDuration, + GenerateSeedance10LiteImageToVideoRequest, + GenerateSeedance10LiteImageToVideoResponse, + GenerateSeedance10LiteResolution, + GenerateVeo2AspectRatio, + GenerateVeo2Duration, + GenerateVeo2ImageToVideoRequest, + GenerateVeo2ImageToVideoResponse, GetInferenceJobStatusError, GetInferenceJobStatusPathInfo, GetInferenceJobStatusSuccessResponse, @@ -569,6 +661,8 @@ use crate::http_server::web_utils::response_success_helpers::*; RedeemBetaKeyError, RedeemBetaKeyRequest, RedeemBetaKeySuccessResponse, + RemoveImageBackgroundRequest, + RemoveImageBackgroundResponse, RenameMediaFileError, RenameMediaFileRequest, RequestDetailsResponse, @@ -636,6 +730,8 @@ use crate::http_server::web_utils::response_success_helpers::*; UploadSavedSceneMediaFileSuccessResponse, UploadSceneSnapshotMediaFileForm, UploadSceneSnapshotMediaFileSuccessResponse, + UploadSnapshotMediaFileForm, + UploadSnapshotMediaFileSuccessResponse, UploadStudioShotFileForm, UploadStudioShotSuccessResponse, UploadVideoMediaSuccessResponse, diff --git a/crates/service/web/storyteller_web/src/http_server/common_responses/common_web_error.rs b/crates/service/web/storyteller_web/src/http_server/common_responses/common_web_error.rs new file mode 100644 index 0000000000..37c18c85f7 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/common_responses/common_web_error.rs @@ -0,0 +1,90 @@ +use actix_http::StatusCode; +use actix_web::{HttpResponse, HttpResponseBuilder, ResponseError}; +use std::error::Error; +use std::fmt::{Display, Formatter}; +use utoipa::ToSchema; + +#[derive(Debug)] +pub enum CommonWebError { + // TODO: Bad input with error fields vector. + /// Bad input with a user-facing error message. + BadInputWithSimpleMessage(String), + /// Entity not found. + NotFound, + /// Not authorized to perform the action. + NotAuthorized, + /// Server error with no reason. + ServerError, +} + +impl Error for CommonWebError {} + +impl Display for CommonWebError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +#[derive(Debug, Serialize)] +struct SerializeSimpleErrorWithoutMessage<'a> { + success: bool, + error_code: u16, + error_code_str: Option<&'a str>, +} + + +#[derive(Debug, Serialize)] +struct SerializeSimpleErrorWithMessage<'a> { + success: bool, + error_code: u16, + error_code_str: Option<&'a str>, + message: &'a str, +} + +impl ResponseError for CommonWebError { + fn status_code(&self) -> StatusCode { + match self { + CommonWebError::BadInputWithSimpleMessage(_) => StatusCode::BAD_REQUEST, + CommonWebError::NotFound => StatusCode::NOT_FOUND, + CommonWebError::NotAuthorized => StatusCode::UNAUTHORIZED, + CommonWebError::ServerError => StatusCode::INTERNAL_SERVER_ERROR, + } + } + + fn error_response(&self) -> HttpResponse { + match self { + CommonWebError::BadInputWithSimpleMessage(msg) => { + HttpResponse::BadRequest() + .json(SerializeSimpleErrorWithMessage { + success: false, + error_code: self.status_code().as_u16(), + error_code_str: self.status_code().canonical_reason(), + message: msg, + }) + }, + _ => { + HttpResponseBuilder::new(self.status_code()) + .json(SerializeSimpleErrorWithoutMessage { + success: false, + error_code: self.status_code().as_u16(), + error_code_str: self.status_code().canonical_reason(), + }) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use actix_http::body::MessageBody; + + #[test] + fn test_bad_input_json() { + let error = CommonWebError::BadInputWithSimpleMessage("foo bar baz".to_string()); + let response = error.error_response(); + let bytes = response.into_body().try_into_bytes().unwrap(); + let body = String::from_utf8(bytes.to_vec()).unwrap(); + assert_eq!(body, "{\"success\":false,\"error_code\":400,\"error_code_str\":\"Bad Request\",\"message\":\"foo bar baz\"}"); + } +} \ No newline at end of file diff --git a/crates/service/web/storyteller_web/src/http_server/common_responses/mod.rs b/crates/service/web/storyteller_web/src/http_server/common_responses/mod.rs index d45b5053e9..5a7d185884 100644 --- a/crates/service/web/storyteller_web/src/http_server/common_responses/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/common_responses/mod.rs @@ -1,3 +1,4 @@ +pub mod common_web_error; pub mod media; pub mod media_file_origin_details; pub mod media_file_social_meta_lite; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/image/generate_flux_1_dev_text_to_image_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/image/generate_flux_1_dev_text_to_image_handler.rs new file mode 100644 index 0000000000..f7f3b02f95 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/image/generate_flux_1_dev_text_to_image_handler.rs @@ -0,0 +1,158 @@ +use std::fmt; +use std::sync::Arc; + +use crate::http_server::common_requests::media_file_token_path_info::MediaFileTokenPathInfo; +use crate::http_server::common_responses::common_web_error::CommonWebError; +use crate::http_server::common_responses::media::media_links::MediaLinks; +use crate::http_server::deprecated_endpoints::engine::create_scene_handler::CreateSceneError; +use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain; +use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError; +use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format; +use crate::http_server::web_utils::response_success_helpers::simple_json_success; +use crate::state::server_state::ServerState; +use crate::util::delete_role_disambiguation::{delete_role_disambiguation, DeleteRole}; +use actix_web::error::ResponseError; +use actix_web::http::StatusCode; +use actix_web::web::Json; +use actix_web::web::Path; +use actix_web::{web, HttpRequest, HttpResponse}; +use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::{GenerateFlux1DevTextToImageAspectRatio, GenerateFlux1DevTextToImageNumImages, GenerateFlux1DevTextToImageRequest, GenerateFlux1DevTextToImageResponse}; +use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath; +use enums::common::visibility::Visibility; +use fal_client::requests::webhook::image::enqueue_flux_1_dev_text_to_image_webhook::enqueue_flux_1_dev_text_to_image_webhook; +use fal_client::requests::webhook::image::enqueue_flux_1_dev_text_to_image_webhook::{Flux1DevArgs, Flux1DevAspectRatio, Flux1DevNumImages}; +use http_server_common::request::get_request_ip::get_request_ip; +use http_server_common::response::serialize_as_json_error::serialize_as_json_error; +use idempotency::uuid::generate_random_uuid; +use log::{error, info, warn}; +use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::insert_generic_inference_job_for_fal_queue; +use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory; +use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::InsertGenericInferenceForFalArgs; +use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token; +use mysql_queries::queries::media_files::get::get_media_file::{get_media_file, MediaFile}; +use tokens::tokens::media_files::MediaFileToken; +use utoipa::ToSchema; + +/// Flux 1 Dev text to image +#[utoipa::path( + post, + tag = "Generate Images", + path = "/v1/generate/image/flux_1_dev_text_to_image", + responses( + (status = 200, description = "Success", body = GenerateFlux1DevTextToImageResponse), + ), + params( + ("request" = GenerateFlux1DevTextToImageRequest, description = "Payload for Request"), + ) +)] +pub async fn generate_flux_1_dev_text_to_image_handler( + http_request: HttpRequest, + request: Json, + server_state: web::Data> +) -> Result, CommonWebError> { + let maybe_user_session = server_state + .session_checker + .maybe_get_user_session(&http_request, &server_state.mysql_pool) + .await + .map_err(|e| { + warn!("Session checker error: {:?}", e); + CommonWebError::ServerError + })?; + + let maybe_avt_token = server_state + .avt_cookie_manager + .get_avt_token_from_request(&http_request); + + // TODO: Limit usage for new accounts. Billing, free credits metering, etc. + + //let user_session = match maybe_user_session { + // Some(session) => session, + // None => { + // warn!("not logged in"); + // return Err(CommonWebError::NotAuthorized); + // } + //}; + + if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) { + return Err(CommonWebError::BadInputWithSimpleMessage(reason)); + } + + insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool) + .await + .map_err(|err| { + error!("Error inserting idempotency token: {:?}", err); + CommonWebError::BadInputWithSimpleMessage("repeated idempotency token".to_string()) + })?; + + const IS_MOD : bool = false; + + info!("Fal webhook URL: {}", server_state.fal.webhook_url); + + let aspect_ratio = match request.aspect_ratio { + Some(GenerateFlux1DevTextToImageAspectRatio::Square) => Flux1DevAspectRatio::Square, + Some(GenerateFlux1DevTextToImageAspectRatio::SquareHd) => Flux1DevAspectRatio::SquareHd, + Some(GenerateFlux1DevTextToImageAspectRatio::LandscapeFourByThree) => Flux1DevAspectRatio::LandscapeFourByThree, + Some(GenerateFlux1DevTextToImageAspectRatio::LandscapeSixteenByNine) => Flux1DevAspectRatio::LandscapeSixteenByNine, + Some(GenerateFlux1DevTextToImageAspectRatio::PortraitThreeByFour) => Flux1DevAspectRatio::PortraitThreeByFour, + Some(GenerateFlux1DevTextToImageAspectRatio::PortraitNineBySixteen) => Flux1DevAspectRatio::PortraitNineBySixteen, + None => Flux1DevAspectRatio::LandscapeSixteenByNine, // Default + }; + + let num_images = match request.num_images { + Some(GenerateFlux1DevTextToImageNumImages::One) => Flux1DevNumImages::One, + Some(GenerateFlux1DevTextToImageNumImages::Two) => Flux1DevNumImages::Two, + Some(GenerateFlux1DevTextToImageNumImages::Three) => Flux1DevNumImages::Three, + Some(GenerateFlux1DevTextToImageNumImages::Four) => Flux1DevNumImages::Four, + None => Flux1DevNumImages::One, // Default + }; + + let args = Flux1DevArgs { + prompt: request.prompt.as_deref().unwrap_or(""), + webhook_url: &server_state.fal.webhook_url, + api_key: &server_state.fal.api_key, + aspect_ratio, + num_images, + }; + + let fal_result = enqueue_flux_1_dev_text_to_image_webhook(args) + .await + .map_err(|err| { + warn!("Error calling enqueue_flux_1_dev_text_to_image_webhook: {:?}", err); + CommonWebError::ServerError + })?; + + let external_job_id = fal_result.request_id + .ok_or_else(|| { + warn!("Fal request_id is None"); + CommonWebError::ServerError + })?; + + info!("Fal request_id: {}", external_job_id); + + let ip_address = get_request_ip(&http_request); + + let db_result = insert_generic_inference_job_for_fal_queue(InsertGenericInferenceForFalArgs { + uuid_idempotency_token: &request.uuid_idempotency_token, + maybe_external_third_party_id: &external_job_id, + fal_category: FalCategory::ImageGeneration, + maybe_inference_args: None, + maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token), + maybe_avt_token: maybe_avt_token.as_ref(), + creator_ip_address: &ip_address, + creator_set_visibility: Visibility::Public, + mysql_pool: &server_state.mysql_pool, + }).await; + + let job_token = match db_result { + Ok(token) => token, + Err(err) => { + warn!("Error inserting generic inference job for FAL queue: {:?}", err); + return Err(CommonWebError::ServerError); + } + }; + + Ok(Json(GenerateFlux1DevTextToImageResponse { + success: true, + inference_job_token: job_token, + })) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/image/generate_flux_1_schnell_text_to_image_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/image/generate_flux_1_schnell_text_to_image_handler.rs new file mode 100644 index 0000000000..7405d92268 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/image/generate_flux_1_schnell_text_to_image_handler.rs @@ -0,0 +1,158 @@ +use std::fmt; +use std::sync::Arc; + +use crate::http_server::common_requests::media_file_token_path_info::MediaFileTokenPathInfo; +use crate::http_server::common_responses::common_web_error::CommonWebError; +use crate::http_server::common_responses::media::media_links::MediaLinks; +use crate::http_server::deprecated_endpoints::engine::create_scene_handler::CreateSceneError; +use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain; +use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError; +use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format; +use crate::http_server::web_utils::response_success_helpers::simple_json_success; +use crate::state::server_state::ServerState; +use crate::util::delete_role_disambiguation::{delete_role_disambiguation, DeleteRole}; +use actix_web::error::ResponseError; +use actix_web::http::StatusCode; +use actix_web::web::Json; +use actix_web::web::Path; +use actix_web::{web, HttpRequest, HttpResponse}; +use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::{GenerateFlux1SchnellTextToImageAspectRatio, GenerateFlux1SchnellTextToImageNumImages, GenerateFlux1SchnellTextToImageRequest, GenerateFlux1SchnellTextToImageResponse}; +use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath; +use enums::common::visibility::Visibility; +use fal_client::requests::webhook::image::enqueue_flux_1_schnell_text_to_image_webhook::enqueue_flux_1_schnell_text_to_image_webhook; +use fal_client::requests::webhook::image::enqueue_flux_1_schnell_text_to_image_webhook::{Flux1SchnellArgs, Flux1SchnellAspectRatio, Flux1SchnellNumImages}; +use http_server_common::request::get_request_ip::get_request_ip; +use http_server_common::response::serialize_as_json_error::serialize_as_json_error; +use idempotency::uuid::generate_random_uuid; +use log::{error, info, warn}; +use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::insert_generic_inference_job_for_fal_queue; +use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory; +use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::InsertGenericInferenceForFalArgs; +use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token; +use mysql_queries::queries::media_files::get::get_media_file::{get_media_file, MediaFile}; +use tokens::tokens::media_files::MediaFileToken; +use utoipa::ToSchema; + +/// Flux 1 Schnell text to image +#[utoipa::path( + post, + tag = "Generate Images", + path = "/v1/generate/image/flux_1_schnell_text_to_image", + responses( + (status = 200, description = "Success", body = GenerateFlux1SchnellTextToImageResponse), + ), + params( + ("request" = GenerateFlux1SchnellTextToImageRequest, description = "Payload for Request"), + ) +)] +pub async fn generate_flux_1_schnell_text_to_image_handler( + http_request: HttpRequest, + request: Json, + server_state: web::Data> +) -> Result, CommonWebError> { + let maybe_user_session = server_state + .session_checker + .maybe_get_user_session(&http_request, &server_state.mysql_pool) + .await + .map_err(|e| { + warn!("Session checker error: {:?}", e); + CommonWebError::ServerError + })?; + + let maybe_avt_token = server_state + .avt_cookie_manager + .get_avt_token_from_request(&http_request); + + // TODO: Limit usage for new accounts. Billing, free credits metering, etc. + + //let user_session = match maybe_user_session { + // Some(session) => session, + // None => { + // warn!("not logged in"); + // return Err(CommonWebError::NotAuthorized); + // } + //}; + + if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) { + return Err(CommonWebError::BadInputWithSimpleMessage(reason)); + } + + insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool) + .await + .map_err(|err| { + error!("Error inserting idempotency token: {:?}", err); + CommonWebError::BadInputWithSimpleMessage("repeated idempotency token".to_string()) + })?; + + const IS_MOD : bool = false; + + info!("Fal webhook URL: {}", server_state.fal.webhook_url); + + let aspect_ratio = match request.aspect_ratio { + Some(GenerateFlux1SchnellTextToImageAspectRatio::Square) => Flux1SchnellAspectRatio::Square, + Some(GenerateFlux1SchnellTextToImageAspectRatio::SquareHd) => Flux1SchnellAspectRatio::SquareHd, + Some(GenerateFlux1SchnellTextToImageAspectRatio::LandscapeFourByThree) => Flux1SchnellAspectRatio::LandscapeFourByThree, + Some(GenerateFlux1SchnellTextToImageAspectRatio::LandscapeSixteenByNine) => Flux1SchnellAspectRatio::LandscapeSixteenByNine, + Some(GenerateFlux1SchnellTextToImageAspectRatio::PortraitThreeByFour) => Flux1SchnellAspectRatio::PortraitThreeByFour, + Some(GenerateFlux1SchnellTextToImageAspectRatio::PortraitNineBySixteen) => Flux1SchnellAspectRatio::PortraitNineBySixteen, + None => Flux1SchnellAspectRatio::LandscapeSixteenByNine, // Default + }; + + let num_images = match request.num_images { + Some(GenerateFlux1SchnellTextToImageNumImages::One) => Flux1SchnellNumImages::One, + Some(GenerateFlux1SchnellTextToImageNumImages::Two) => Flux1SchnellNumImages::Two, + Some(GenerateFlux1SchnellTextToImageNumImages::Three) => Flux1SchnellNumImages::Three, + Some(GenerateFlux1SchnellTextToImageNumImages::Four) => Flux1SchnellNumImages::Four, + None => Flux1SchnellNumImages::One, // Default + }; + + let args = Flux1SchnellArgs { + prompt: request.prompt.as_deref().unwrap_or(""), + webhook_url: &server_state.fal.webhook_url, + api_key: &server_state.fal.api_key, + aspect_ratio, + num_images, + }; + + let fal_result = enqueue_flux_1_schnell_text_to_image_webhook(args) + .await + .map_err(|err| { + warn!("Error calling enqueue_flux_1_schnell_text_to_image_webhook: {:?}", err); + CommonWebError::ServerError + })?; + + let external_job_id = fal_result.request_id + .ok_or_else(|| { + warn!("Fal request_id is None"); + CommonWebError::ServerError + })?; + + info!("Fal request_id: {}", external_job_id); + + let ip_address = get_request_ip(&http_request); + + let db_result = insert_generic_inference_job_for_fal_queue(InsertGenericInferenceForFalArgs { + uuid_idempotency_token: &request.uuid_idempotency_token, + maybe_external_third_party_id: &external_job_id, + fal_category: FalCategory::ImageGeneration, + maybe_inference_args: None, + maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token), + maybe_avt_token: maybe_avt_token.as_ref(), + creator_ip_address: &ip_address, + creator_set_visibility: Visibility::Public, + mysql_pool: &server_state.mysql_pool, + }).await; + + let job_token = match db_result { + Ok(token) => token, + Err(err) => { + warn!("Error inserting generic inference job for FAL queue: {:?}", err); + return Err(CommonWebError::ServerError); + } + }; + + Ok(Json(GenerateFlux1SchnellTextToImageResponse { + success: true, + inference_job_token: job_token, + })) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/image/generate_flux_pro_11_text_to_image_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/image/generate_flux_pro_11_text_to_image_handler.rs new file mode 100644 index 0000000000..d54f14ff90 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/image/generate_flux_pro_11_text_to_image_handler.rs @@ -0,0 +1,159 @@ +use std::fmt; +use std::sync::Arc; + +use crate::http_server::common_requests::media_file_token_path_info::MediaFileTokenPathInfo; +use crate::http_server::common_responses::common_web_error::CommonWebError; +use crate::http_server::common_responses::media::media_links::MediaLinks; +use crate::http_server::deprecated_endpoints::engine::create_scene_handler::CreateSceneError; +use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain; +use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError; +use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format; +use crate::http_server::web_utils::response_success_helpers::simple_json_success; +use crate::state::server_state::ServerState; +use crate::util::delete_role_disambiguation::{delete_role_disambiguation, DeleteRole}; +use actix_web::error::ResponseError; +use actix_web::http::StatusCode; +use actix_web::web::Json; +use actix_web::web::Path; +use actix_web::{web, HttpRequest, HttpResponse}; +use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageResponse; +use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::{GenerateFluxPro11TextToImageAspectRatio, GenerateFluxPro11TextToImageNumImages, GenerateFluxPro11TextToImageRequest}; +use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath; +use enums::common::visibility::Visibility; +use fal_client::requests::webhook::image::enqueue_flux_pro_11_text_to_image_webhook::FluxPro11Args; +use fal_client::requests::webhook::image::enqueue_flux_pro_11_text_to_image_webhook::{enqueue_flux_pro_11_text_to_image_webhook, FluxPro11AspectRatio, FluxPro11NumImages}; +use http_server_common::request::get_request_ip::get_request_ip; +use http_server_common::response::serialize_as_json_error::serialize_as_json_error; +use idempotency::uuid::generate_random_uuid; +use log::{error, info, warn}; +use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::insert_generic_inference_job_for_fal_queue; +use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory; +use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::InsertGenericInferenceForFalArgs; +use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token; +use mysql_queries::queries::media_files::get::get_media_file::{get_media_file, MediaFile}; +use tokens::tokens::media_files::MediaFileToken; +use utoipa::ToSchema; + + +/// Flux Pro 1.1 +#[utoipa::path( + post, + tag = "Generate Images", + path = "/v1/generate/image/flux_pro_1.1_text_to_image", + responses( + (status = 200, description = "Success", body = GenerateFluxPro11TextToImageResponse), + ), + params( + ("request" = GenerateFluxPro11TextToImageRequest, description = "Payload for Request"), + ) +)] +pub async fn generate_flux_pro_11_text_to_image_handler( + http_request: HttpRequest, + request: Json, + server_state: web::Data> +) -> Result, CommonWebError> { + let maybe_user_session = server_state + .session_checker + .maybe_get_user_session(&http_request, &server_state.mysql_pool) + .await + .map_err(|e| { + warn!("Session checker error: {:?}", e); + CommonWebError::ServerError + })?; + + let maybe_avt_token = server_state + .avt_cookie_manager + .get_avt_token_from_request(&http_request); + + // TODO: Limit usage for new accounts. Billing, free credits metering, etc. + + //let user_session = match maybe_user_session { + // Some(session) => session, + // None => { + // warn!("not logged in"); + // return Err(GenerateFluxProUltraTextToImageError::NotAuthorized); + // } + //}; + + if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) { + return Err(CommonWebError::BadInputWithSimpleMessage(reason)); + } + + insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool) + .await + .map_err(|err| { + error!("Error inserting idempotency token: {:?}", err); + CommonWebError::BadInputWithSimpleMessage("invalid idempotency token".to_string()) + })?; + + const IS_MOD : bool = false; + + info!("Fal webhook URL: {}", server_state.fal.webhook_url); + + let aspect_ratio = match request.aspect_ratio { + Some(GenerateFluxPro11TextToImageAspectRatio::Square) => FluxPro11AspectRatio::Square, + Some(GenerateFluxPro11TextToImageAspectRatio::LandscapeFourByThree) => FluxPro11AspectRatio::LandscapeFourByThree, + Some(GenerateFluxPro11TextToImageAspectRatio::LandscapeSixteenByNine) => FluxPro11AspectRatio::LandscapeSixteenByNine, + Some(GenerateFluxPro11TextToImageAspectRatio::PortraitThreeByFour) => FluxPro11AspectRatio::PortraitThreeByFour, + Some(GenerateFluxPro11TextToImageAspectRatio::PortraitNineBySixteen) => FluxPro11AspectRatio::PortraitNineBySixteen, + None => FluxPro11AspectRatio::LandscapeSixteenByNine, // Default + }; + + let num_images = match request.num_images { + Some(GenerateFluxPro11TextToImageNumImages::One) => FluxPro11NumImages::One, + Some(GenerateFluxPro11TextToImageNumImages::Two) => FluxPro11NumImages::Two, + Some(GenerateFluxPro11TextToImageNumImages::Three) => FluxPro11NumImages::Three, + Some(GenerateFluxPro11TextToImageNumImages::Four) => FluxPro11NumImages::Four, + None => FluxPro11NumImages::One, // Default + }; + + let args = FluxPro11Args { + prompt: request.prompt.as_deref().unwrap_or(""), + webhook_url: &server_state.fal.webhook_url, + api_key: &server_state.fal.api_key, + aspect_ratio, + num_images, + }; + + let fal_result = enqueue_flux_pro_11_text_to_image_webhook(args) + .await + .map_err(|err| { + warn!("Error calling enqueue_flux_pro_text_to_image_webhook: {:?}", err); + CommonWebError::ServerError + })?; + + let external_job_id = fal_result.request_id + .ok_or_else(|| { + warn!("Fal request_id is None"); + CommonWebError::ServerError + })?; + + info!("Fal request_id: {}", external_job_id); + + let ip_address = get_request_ip(&http_request); + + let db_result = insert_generic_inference_job_for_fal_queue(InsertGenericInferenceForFalArgs { + uuid_idempotency_token: &request.uuid_idempotency_token, + maybe_external_third_party_id: &external_job_id, + fal_category: FalCategory::ImageGeneration, + maybe_inference_args: None, + maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token), + maybe_avt_token: maybe_avt_token.as_ref(), + creator_ip_address: &ip_address, + creator_set_visibility: Visibility::Public, + mysql_pool: &server_state.mysql_pool, + }).await; + + let job_token = match db_result { + Ok(token) => token, + Err(err) => { + warn!("Error inserting generic inference job for FAL queue: {:?}", err); + return Err(CommonWebError::ServerError); + } + }; + + Ok(Json(GenerateFluxPro11TextToImageResponse { + success: true, + inference_job_token: job_token, + })) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/image/generate_flux_pro_ultra_text_to_image_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/image/generate_flux_pro_11_ultra_text_to_image_handler.rs similarity index 70% rename from crates/service/web/storyteller_web/src/http_server/endpoints/generate/image/generate_flux_pro_ultra_text_to_image_handler.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/generate/image/generate_flux_pro_11_ultra_text_to_image_handler.rs index c951d688ff..b6c54e119a 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/image/generate_flux_pro_ultra_text_to_image_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/image/generate_flux_pro_11_ultra_text_to_image_handler.rs @@ -2,6 +2,7 @@ use std::fmt; use std::sync::Arc; use crate::http_server::common_requests::media_file_token_path_info::MediaFileTokenPathInfo; +use crate::http_server::common_responses::common_web_error::CommonWebError; use crate::http_server::common_responses::media::media_links::MediaLinks; use crate::http_server::deprecated_endpoints::engine::create_scene_handler::CreateSceneError; use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain; @@ -15,12 +16,12 @@ 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 artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::{GenerateFluxPro11UltraTextToImageAspectRatio, GenerateFluxPro11UltraTextToImageNumImages, GenerateFluxPro11UltraTextToImageRequest}; use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath; use enums::common::visibility::Visibility; -use fal_client::requests::webhook::image::enqueue_flux_pro_11_ultra_text_to_image_webhook::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 fal_client::requests::webhook::image::enqueue_flux_pro_11_ultra_text_to_image_webhook::{enqueue_flux_pro_11_ultra_text_to_image_webhook, FluxPro11UltraAspectRatio, FluxPro11UltraNumImages}; use http_server_common::request::get_request_ip::get_request_ip; use http_server_common::response::serialize_as_json_error::serialize_as_json_error; use idempotency::uuid::generate_random_uuid; @@ -33,68 +34,31 @@ use mysql_queries::queries::media_files::get::get_media_file::{get_media_file, M use tokens::tokens::media_files::MediaFileToken; use utoipa::ToSchema; -// =============== Error Response =============== - -#[derive(Debug, Serialize, ToSchema)] -pub enum 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 +/// Flux Pro 1.1 Ultra #[utoipa::path( post, tag = "Generate Images", path = "/v1/generate/image/flux_pro_1.1_ultra_text_to_image", responses( - (status = 200, description = "Success", body = GenerateFluxProUltraTextToImageResponse), - (status = 400, description = "Bad input", body = GenerateFluxProUltraTextToImageError), - (status = 401, description = "Not authorized", body = GenerateFluxProUltraTextToImageError), - (status = 500, description = "Server error", body = GenerateFluxProUltraTextToImageError), + (status = 200, description = "Success", body = GenerateFluxPro11UltraTextToImageResponse), ), params( - ("request" = GenerateFluxProUltraTextToImageRequest, description = "Payload for Request"), + ("request" = GenerateFluxPro11UltraTextToImageRequest, description = "Payload for Request"), ) )] pub async fn generate_flux_pro_11_ultra_text_to_image_handler( http_request: HttpRequest, request: Json, server_state: web::Data> -) -> Result, GenerateFluxProUltraTextToImageError> { +) -> Result, CommonWebError> { let maybe_user_session = server_state .session_checker .maybe_get_user_session(&http_request, &server_state.mysql_pool) .await .map_err(|e| { warn!("Session checker error: {:?}", e); - GenerateFluxProUltraTextToImageError::ServerError + CommonWebError::ServerError })?; let maybe_avt_token = server_state @@ -112,36 +76,56 @@ pub async fn generate_flux_pro_11_ultra_text_to_image_handler( //}; if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) { - return Err(GenerateFluxProUltraTextToImageError::BadInput(reason)); + return Err(CommonWebError::BadInputWithSimpleMessage(reason)); } insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool) .await .map_err(|err| { error!("Error inserting idempotency token: {:?}", err); - GenerateFluxProUltraTextToImageError::BadInput("invalid idempotency token".to_string()) + CommonWebError::BadInputWithSimpleMessage("invalid idempotency token".to_string()) })?; + const IS_MOD : bool = false; info!("Fal webhook URL: {}", server_state.fal.webhook_url); + let aspect_ratio = match request.aspect_ratio { + Some(GenerateFluxPro11UltraTextToImageAspectRatio::Square) => FluxPro11UltraAspectRatio::Square, + Some(GenerateFluxPro11UltraTextToImageAspectRatio::LandscapeFourByThree) => FluxPro11UltraAspectRatio::LandscapeFourByThree, + Some(GenerateFluxPro11UltraTextToImageAspectRatio::LandscapeSixteenByNine) => FluxPro11UltraAspectRatio::LandscapeSixteenByNine, + Some(GenerateFluxPro11UltraTextToImageAspectRatio::PortraitThreeByFour) => FluxPro11UltraAspectRatio::PortraitThreeByFour, + Some(GenerateFluxPro11UltraTextToImageAspectRatio::PortraitNineBySixteen) => FluxPro11UltraAspectRatio::PortraitNineBySixteen, + None => FluxPro11UltraAspectRatio::LandscapeSixteenByNine, // Default + }; + + let num_images = match request.num_images { + Some(GenerateFluxPro11UltraTextToImageNumImages::One) => FluxPro11UltraNumImages::One, + Some(GenerateFluxPro11UltraTextToImageNumImages::Two) => FluxPro11UltraNumImages::Two, + Some(GenerateFluxPro11UltraTextToImageNumImages::Three) => FluxPro11UltraNumImages::Three, + Some(GenerateFluxPro11UltraTextToImageNumImages::Four) => FluxPro11UltraNumImages::Four, + None => FluxPro11UltraNumImages::One, // Default + }; + let args = FluxPro11UltraArgs { prompt: request.prompt.as_deref().unwrap_or(""), webhook_url: &server_state.fal.webhook_url, api_key: &server_state.fal.api_key, + aspect_ratio, + num_images, }; let fal_result = enqueue_flux_pro_11_ultra_text_to_image_webhook(args) .await .map_err(|err| { warn!("Error calling enqueue_flux_pro_ultra_text_to_image_webhook: {:?}", err); - GenerateFluxProUltraTextToImageError::ServerError + CommonWebError::ServerError })?; let external_job_id = fal_result.request_id .ok_or_else(|| { warn!("Fal request_id is None"); - GenerateFluxProUltraTextToImageError::ServerError + CommonWebError::ServerError })?; info!("Fal request_id: {}", external_job_id); @@ -164,7 +148,7 @@ pub async fn generate_flux_pro_11_ultra_text_to_image_handler( Ok(token) => token, Err(err) => { warn!("Error inserting generic inference job for FAL queue: {:?}", err); - return Err(GenerateFluxProUltraTextToImageError::ServerError); + return Err(CommonWebError::ServerError); } }; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/image/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/image/mod.rs index 48b150f9f1..0cc8a66f28 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/image/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/image/mod.rs @@ -1,2 +1,5 @@ -pub mod generate_flux_pro_ultra_text_to_image_handler; +pub mod generate_flux_1_dev_text_to_image_handler; +pub mod generate_flux_1_schnell_text_to_image_handler; +pub mod generate_flux_pro_11_text_to_image_handler; +pub mod generate_flux_pro_11_ultra_text_to_image_handler; pub mod remove_image_background_handler; \ No newline at end of file diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/image/remove_image_background_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/image/remove_image_background_handler.rs index 9c2566bc53..ce58105a18 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/image/remove_image_background_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/image/remove_image_background_handler.rs @@ -2,9 +2,12 @@ use std::fmt; use std::sync::Arc; use crate::http_server::common_requests::media_file_token_path_info::MediaFileTokenPathInfo; +use crate::http_server::common_responses::common_web_error::CommonWebError; use crate::http_server::common_responses::media::media_links::MediaLinks; use crate::http_server::deprecated_endpoints::engine::create_scene_handler::CreateSceneError; use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain; +use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError; +use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format; use crate::http_server::web_utils::response_success_helpers::simple_json_success; use crate::state::server_state::ServerState; use crate::util::delete_role_disambiguation::{delete_role_disambiguation, DeleteRole}; @@ -29,42 +32,6 @@ use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_ use mysql_queries::queries::media_files::get::get_media_file::{get_media_file, MediaFile}; use tokens::tokens::media_files::MediaFileToken; use utoipa::ToSchema; -use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError; -use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format; -// =============== Error Response =============== - - -#[derive(Debug, Serialize, ToSchema)] -pub enum RemoveImageBackgroundError { - BadInput(String), - NotFound, - NotAuthorized, - ServerError, -} - -impl ResponseError for RemoveImageBackgroundError { - fn status_code(&self) -> StatusCode { - match *self { - RemoveImageBackgroundError::BadInput(_) => StatusCode::BAD_REQUEST, - RemoveImageBackgroundError::NotFound => StatusCode::NOT_FOUND, - RemoveImageBackgroundError::NotAuthorized => StatusCode::UNAUTHORIZED, - RemoveImageBackgroundError::ServerError => StatusCode::INTERNAL_SERVER_ERROR, - } - } - - fn error_response(&self) -> HttpResponse { - serialize_as_json_error(self) - } -} - -// NB: Not using derive_more::Display since Clion doesn't understand it. -impl fmt::Display for RemoveImageBackgroundError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:?}", self) - } -} - -// =============== Handler =============== /// Background removal #[utoipa::path( @@ -73,9 +40,6 @@ impl fmt::Display for RemoveImageBackgroundError { path = "/v1/generate/image/remove_background", responses( (status = 200, description = "Success", body = RemoveImageBackgroundResponse), - (status = 400, description = "Bad input", body = RemoveImageBackgroundError), - (status = 401, description = "Not authorized", body = RemoveImageBackgroundError), - (status = 500, description = "Server error", body = RemoveImageBackgroundError), ), params( ("request" = RemoveImageBackgroundRequest, description = "Payload for Request"), @@ -85,14 +49,14 @@ pub async fn remove_image_background_handler( http_request: HttpRequest, request: Json, server_state: web::Data> -) -> Result, RemoveImageBackgroundError> { +) -> Result, CommonWebError> { let maybe_user_session = server_state .session_checker .maybe_get_user_session(&http_request, &server_state.mysql_pool) .await .map_err(|e| { warn!("Session checker error: {:?}", e); - RemoveImageBackgroundError::ServerError + CommonWebError::ServerError })?; let maybe_avt_token = server_state @@ -113,19 +77,19 @@ pub async fn remove_image_background_handler( Some(token) => token, None => { warn!("No media file token provided"); - return Err(RemoveImageBackgroundError::BadInput("No media file token provided".to_string())); + return Err(CommonWebError::BadInputWithSimpleMessage("No media file token provided".to_string())); } }; if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) { - return Err(RemoveImageBackgroundError::BadInput(reason)); + return Err(CommonWebError::BadInputWithSimpleMessage(reason)); } insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool) .await .map_err(|err| { error!("Error inserting idempotency token: {:?}", err); - RemoveImageBackgroundError::BadInput("invalid idempotency token".to_string()) + CommonWebError::BadInputWithSimpleMessage("invalid idempotency token".to_string()) })?; const IS_MOD : bool = false; @@ -139,16 +103,16 @@ pub async fn remove_image_background_handler( Ok(Some(media_file)) => media_file, Ok(None) => { warn!("MediaFile not found: {:?}", media_file_token); - return Err(RemoveImageBackgroundError::NotFound); + return Err(CommonWebError::NotFound); }, Err(err) => { warn!("Error looking up media_file: {:?}", err); - return Err(RemoveImageBackgroundError::ServerError); + return Err(CommonWebError::ServerError); } }; if !media_file.media_type.is_jpg_or_png_or_legacy_image() { - return Err(RemoveImageBackgroundError::BadInput("Media file must be a JPG or PNG image".to_string())); + return Err(CommonWebError::BadInputWithSimpleMessage("Media file must be a JPG or PNG image".to_string())); } let media_domain = get_media_domain(&http_request); @@ -175,13 +139,13 @@ pub async fn remove_image_background_handler( .await .map_err(|err| { warn!("Error calling remove_background_rembg_webhook: {:?}", err); - RemoveImageBackgroundError::ServerError + CommonWebError::ServerError })?; let external_job_id = fal_result.request_id .ok_or_else(|| { warn!("Fal request_id is None"); - RemoveImageBackgroundError::ServerError + CommonWebError::ServerError })?; info!("Fal request_id: {}", external_job_id); @@ -204,7 +168,7 @@ pub async fn remove_image_background_handler( Ok(token) => token, Err(err) => { warn!("Error inserting generic inference job for FAL queue: {:?}", err); - return Err(RemoveImageBackgroundError::ServerError); + return Err(CommonWebError::ServerError); } }; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/object/generate_hunyuan_21_image_to_3d_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/object/generate_hunyuan_21_image_to_3d_handler.rs new file mode 100644 index 0000000000..17f2e415f1 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/object/generate_hunyuan_21_image_to_3d_handler.rs @@ -0,0 +1,180 @@ +use std::fmt; +use std::sync::Arc; + +use crate::http_server::common_requests::media_file_token_path_info::MediaFileTokenPathInfo; +use crate::http_server::common_responses::common_web_error::CommonWebError; +use crate::http_server::common_responses::media::media_links::MediaLinks; +use crate::http_server::deprecated_endpoints::engine::create_scene_handler::CreateSceneError; +use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain; +use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError; +use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format; +use crate::http_server::web_utils::response_success_helpers::simple_json_success; +use crate::state::server_state::ServerState; +use crate::util::delete_role_disambiguation::{delete_role_disambiguation, DeleteRole}; +use actix_web::error::ResponseError; +use actix_web::http::StatusCode; +use actix_web::web::Json; +use actix_web::web::Path; +use actix_web::{web, HttpRequest, HttpResponse}; +use artcraft_api_defs::generate::object::generate_hunyuan_21_image_to_3d::GenerateHunyuan21ImageTo3dRequest; +use artcraft_api_defs::generate::object::generate_hunyuan_21_image_to_3d::GenerateHunyuan21ImageTo3dResponse; +use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath; +use enums::common::visibility::Visibility; +use fal_client::requests::webhook::object::enqueue_hunyuan_3d_21_image_to_3d_webhook::enqueue_hunyuan_3d_2_1_image_to_3d_webhook; +use fal_client::requests::webhook::object::enqueue_hunyuan_3d_21_image_to_3d_webhook::Hunyuan3d21Args; +use http_server_common::request::get_request_ip::get_request_ip; +use http_server_common::response::serialize_as_json_error::serialize_as_json_error; +use idempotency::uuid::generate_random_uuid; +use log::{error, info, warn}; +use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::insert_generic_inference_job_for_fal_queue; +use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory; +use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::InsertGenericInferenceForFalArgs; +use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token; +use mysql_queries::queries::media_files::get::get_media_file::{get_media_file, MediaFile}; +use tokens::tokens::media_files::MediaFileToken; +use utoipa::ToSchema; + +/// Hunyuan 2.1 Image to 3D +#[utoipa::path( + post, + tag = "Generate Objects", + path = "/v1/generate/object/hunyuan_2.1_image_to_3d", + responses( + (status = 200, description = "Success", body = GenerateHunyuan21ImageTo3dResponse), + ), + params( + ("request" = GenerateHunyuan21ImageTo3dRequest, description = "Payload for Request"), + ) +)] +pub async fn generate_hunyuan_21_image_to_3d_handler( + http_request: HttpRequest, + request: Json, + server_state: web::Data> +) -> Result, CommonWebError> { + let maybe_user_session = server_state + .session_checker + .maybe_get_user_session(&http_request, &server_state.mysql_pool) + .await + .map_err(|e| { + warn!("Session checker error: {:?}", e); + CommonWebError::ServerError + })?; + + let maybe_avt_token = server_state + .avt_cookie_manager + .get_avt_token_from_request(&http_request); + + // TODO: Limit usage for new accounts. Billing, free credits metering, etc. + + //let user_session = match maybe_user_session { + // Some(session) => session, + // None => { + // warn!("not logged in"); + // return Err(CommonWebError::NotAuthorized); + // } + //}; + + let media_file_token = match &request.media_file_token { + Some(token) => token, + None => { + warn!("No media file token provided"); + return Err(CommonWebError::BadInputWithSimpleMessage("No media file token provided".to_string())); + } + }; + + if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) { + return Err(CommonWebError::BadInputWithSimpleMessage(reason)); + } + + insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool) + .await + .map_err(|err| { + error!("Error inserting idempotency token: {:?}", err); + CommonWebError::BadInputWithSimpleMessage("invalid idempotency token".to_string()) + })?; + const IS_MOD : bool = false; + + let media_file_lookup_result = get_media_file( + media_file_token, + IS_MOD, + &server_state.mysql_pool, + ).await; + + let media_file = match media_file_lookup_result { + Ok(Some(media_file)) => media_file, + Ok(None) => { + warn!("MediaFile not found: {:?}", media_file_token); + return Err(CommonWebError::NotFound); + }, + Err(err) => { + warn!("Error looking up media_file: {:?}", err); + return Err(CommonWebError::ServerError); + } + }; + + if !media_file.media_type.is_jpg_or_png_or_legacy_image() { + return Err(CommonWebError::BadInputWithSimpleMessage("Media file must be a JPG or PNG image".to_string())); + } + + let media_domain = get_media_domain(&http_request); + + let bucket_path = MediaFileBucketPath::from_object_hash( + &media_file.public_bucket_directory_hash, + media_file.maybe_public_bucket_prefix.as_deref(), + media_file.maybe_public_bucket_extension.as_deref()); + + let media_links = MediaLinks::from_media_path_and_env( + media_domain, + server_state.server_environment, + &bucket_path); + + info!("Fal webhook URL: {}", server_state.fal.webhook_url); + + let args = Hunyuan3d21Args { + image_url: media_links.cdn_url, + webhook_url: &server_state.fal.webhook_url, + api_key: &server_state.fal.api_key, + }; + + let fal_result = enqueue_hunyuan_3d_2_1_image_to_3d_webhook(args) + .await + .map_err(|err| { + warn!("Error calling enqueue_hunyuan_3d_2_1_image_to_3d_webhook: {:?}", err); + CommonWebError::ServerError + })?; + + let external_job_id = fal_result.request_id + .ok_or_else(|| { + warn!("Fal request_id is None"); + CommonWebError::ServerError + })?; + + info!("Fal request_id: {}", external_job_id); + + let ip_address = get_request_ip(&http_request); + + let db_result = insert_generic_inference_job_for_fal_queue(InsertGenericInferenceForFalArgs { + uuid_idempotency_token: &request.uuid_idempotency_token, + maybe_external_third_party_id: &external_job_id, + fal_category: FalCategory::ObjectGeneration, + maybe_inference_args: None, + maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token), + maybe_avt_token: maybe_avt_token.as_ref(), + creator_ip_address: &ip_address, + creator_set_visibility: Visibility::Public, + mysql_pool: &server_state.mysql_pool, + }).await; + + let job_token = match db_result { + Ok(token) => token, + Err(err) => { + warn!("Error inserting generic inference job for FAL queue: {:?}", err); + return Err(CommonWebError::ServerError); + } + }; + + Ok(Json(GenerateHunyuan21ImageTo3dResponse { + success: true, + inference_job_token: job_token, + })) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/object/generate_hunyuan2_image_to_3d_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/object/generate_hunyuan_2_image_to_3d_handler.rs similarity index 74% rename from crates/service/web/storyteller_web/src/http_server/endpoints/generate/object/generate_hunyuan2_image_to_3d_handler.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/generate/object/generate_hunyuan_2_image_to_3d_handler.rs index 14a824ae92..7dd60d38a0 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/object/generate_hunyuan2_image_to_3d_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/object/generate_hunyuan_2_image_to_3d_handler.rs @@ -2,6 +2,7 @@ use std::fmt; use std::sync::Arc; use crate::http_server::common_requests::media_file_token_path_info::MediaFileTokenPathInfo; +use crate::http_server::common_responses::common_web_error::CommonWebError; use crate::http_server::common_responses::media::media_links::MediaLinks; use crate::http_server::deprecated_endpoints::engine::create_scene_handler::CreateSceneError; use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain; @@ -33,51 +34,13 @@ use mysql_queries::queries::media_files::get::get_media_file::{get_media_file, M use tokens::tokens::media_files::MediaFileToken; use utoipa::ToSchema; -// =============== Error Response =============== - - -#[derive(Debug, Serialize, ToSchema)] -pub enum GenerateHunyuan2ImageTo3dError { - BadInput(String), - NotFound, - NotAuthorized, - ServerError, -} - -impl ResponseError for GenerateHunyuan2ImageTo3dError { - fn status_code(&self) -> StatusCode { - match *self { - GenerateHunyuan2ImageTo3dError::BadInput(_) => StatusCode::BAD_REQUEST, - GenerateHunyuan2ImageTo3dError::NotFound => StatusCode::NOT_FOUND, - GenerateHunyuan2ImageTo3dError::NotAuthorized => StatusCode::UNAUTHORIZED, - GenerateHunyuan2ImageTo3dError::ServerError => StatusCode::INTERNAL_SERVER_ERROR, - } - } - - fn error_response(&self) -> HttpResponse { - serialize_as_json_error(self) - } -} - -// NB: Not using derive_more::Display since Clion doesn't understand it. -impl fmt::Display for GenerateHunyuan2ImageTo3dError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:?}", self) - } -} - -// =============== Handler =============== - -/// Background removal +/// Hunyuan 2.0 Image to 3D #[utoipa::path( post, tag = "Generate Objects", path = "/v1/generate/object/hunyuan_2_image_to_3d", responses( (status = 200, description = "Success", body = GenerateHunyuan2ImageTo3dResponse), - (status = 400, description = "Bad input", body = GenerateHunyuan2ImageTo3dError), - (status = 401, description = "Not authorized", body = GenerateHunyuan2ImageTo3dError), - (status = 500, description = "Server error", body = GenerateHunyuan2ImageTo3dError), ), params( ("request" = GenerateHunyuan2ImageTo3dRequest, description = "Payload for Request"), @@ -87,14 +50,14 @@ pub async fn generate_hunyuan_2_image_to_3d_handler( http_request: HttpRequest, request: Json, server_state: web::Data> -) -> Result, GenerateHunyuan2ImageTo3dError> { +) -> Result, CommonWebError> { let maybe_user_session = server_state .session_checker .maybe_get_user_session(&http_request, &server_state.mysql_pool) .await .map_err(|e| { warn!("Session checker error: {:?}", e); - GenerateHunyuan2ImageTo3dError::ServerError + CommonWebError::ServerError })?; let maybe_avt_token = server_state @@ -107,7 +70,7 @@ pub async fn generate_hunyuan_2_image_to_3d_handler( // Some(session) => session, // None => { // warn!("not logged in"); - // return Err(GenerateHunyuan2ImageTo3dError::NotAuthorized); + // return Err(CommonWebError::NotAuthorized); // } //}; @@ -115,19 +78,19 @@ pub async fn generate_hunyuan_2_image_to_3d_handler( Some(token) => token, None => { warn!("No media file token provided"); - return Err(GenerateHunyuan2ImageTo3dError::BadInput("No media file token provided".to_string())); + return Err(CommonWebError::BadInputWithSimpleMessage("No media file token provided".to_string())); } }; if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) { - return Err(GenerateHunyuan2ImageTo3dError::BadInput(reason)); + return Err(CommonWebError::BadInputWithSimpleMessage(reason)); } insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool) .await .map_err(|err| { error!("Error inserting idempotency token: {:?}", err); - GenerateHunyuan2ImageTo3dError::BadInput("invalid idempotency token".to_string()) + CommonWebError::BadInputWithSimpleMessage("invalid idempotency token".to_string()) })?; const IS_MOD : bool = false; @@ -141,16 +104,16 @@ pub async fn generate_hunyuan_2_image_to_3d_handler( Ok(Some(media_file)) => media_file, Ok(None) => { warn!("MediaFile not found: {:?}", media_file_token); - return Err(GenerateHunyuan2ImageTo3dError::NotFound); + return Err(CommonWebError::NotFound); }, Err(err) => { warn!("Error looking up media_file: {:?}", err); - return Err(GenerateHunyuan2ImageTo3dError::ServerError); + return Err(CommonWebError::ServerError); } }; if !media_file.media_type.is_jpg_or_png_or_legacy_image() { - return Err(GenerateHunyuan2ImageTo3dError::BadInput("Media file must be a JPG or PNG image".to_string())); + return Err(CommonWebError::BadInputWithSimpleMessage("Media file must be a JPG or PNG image".to_string())); } let media_domain = get_media_domain(&http_request); @@ -177,13 +140,13 @@ pub async fn generate_hunyuan_2_image_to_3d_handler( .await .map_err(|err| { warn!("Error calling enqueue_hunyuan2_image_to_3d_webhook: {:?}", err); - GenerateHunyuan2ImageTo3dError::ServerError + CommonWebError::ServerError })?; let external_job_id = fal_result.request_id .ok_or_else(|| { warn!("Fal request_id is None"); - GenerateHunyuan2ImageTo3dError::ServerError + CommonWebError::ServerError })?; info!("Fal request_id: {}", external_job_id); @@ -206,7 +169,7 @@ pub async fn generate_hunyuan_2_image_to_3d_handler( Ok(token) => token, Err(err) => { warn!("Error inserting generic inference job for FAL queue: {:?}", err); - return Err(GenerateHunyuan2ImageTo3dError::ServerError); + return Err(CommonWebError::ServerError); } }; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/object/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/object/mod.rs index 4d56b7015c..fc06b7c299 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/object/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/object/mod.rs @@ -1 +1,2 @@ -pub mod generate_hunyuan2_image_to_3d_handler; \ No newline at end of file +pub mod generate_hunyuan_21_image_to_3d_handler; +pub mod generate_hunyuan_2_image_to_3d_handler; \ No newline at end of file diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/generate_kling_1_6_pro_video_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/generate_kling_1_6_pro_video_handler.rs index 5fb024db90..f20d0d1add 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/generate_kling_1_6_pro_video_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/generate_kling_1_6_pro_video_handler.rs @@ -2,6 +2,7 @@ use std::fmt; use std::sync::Arc; use crate::http_server::common_requests::media_file_token_path_info::MediaFileTokenPathInfo; +use crate::http_server::common_responses::common_web_error::CommonWebError; use crate::http_server::common_responses::media::media_links::MediaLinks; use crate::http_server::deprecated_endpoints::engine::create_scene_handler::CreateSceneError; use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain; @@ -15,8 +16,8 @@ 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_pro_image_to_video::{GenerateKling16ProDuration, GenerateKling16ProImageToVideoResponse}; 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; @@ -34,10 +35,6 @@ use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_ use mysql_queries::queries::media_files::get::get_media_file::{get_media_file, MediaFile}; use tokens::tokens::media_files::MediaFileToken; use utoipa::ToSchema; - -// =============== Error Response =============== - - /* TODO: Either on the Tauri client or as an API, make "generic" image / video / object endpoints. Take a reference image payload: @@ -61,65 +58,30 @@ use utoipa::ToSchema; */ -#[derive(Debug, Serialize, ToSchema)] -pub enum GenerateKling16VideoError { - BadInput(String), - NotFound, - NotAuthorized, - ServerError, -} - -impl ResponseError for GenerateKling16VideoError { - fn status_code(&self) -> StatusCode { - match *self { - GenerateKling16VideoError::BadInput(_) => StatusCode::BAD_REQUEST, - GenerateKling16VideoError::NotFound => StatusCode::NOT_FOUND, - GenerateKling16VideoError::NotAuthorized => StatusCode::UNAUTHORIZED, - GenerateKling16VideoError::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 GenerateKling16VideoError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:?}", self) - } -} - -// =============== Handler =============== - -/// Background removal +/// Kling 1.6 Pro Image to Video #[utoipa::path( post, tag = "Generate Videos", path = "/v1/generate/video/kling_1.6_pro_image_to_video", responses( - (status = 200, description = "Success", body = RemoveImageBackgroundResponse), - (status = 400, description = "Bad input", body = RemoveImageBackgroundError), - (status = 401, description = "Not authorized", body = RemoveImageBackgroundError), - (status = 500, description = "Server error", body = RemoveImageBackgroundError), + (status = 200, description = "Success", body = GenerateKling16ProImageToVideoResponse), ), params( - ("request" = RemoveImageBackgroundRequest, description = "Payload for Request"), + ("request" = GenerateKling16ProImageToVideoRequest, description = "Payload for Request"), ) )] pub async fn generate_kling_1_6_pro_video_handler( http_request: HttpRequest, request: Json, server_state: web::Data> -) -> Result, GenerateKling16VideoError> { +) -> Result, CommonWebError> { let maybe_user_session = server_state .session_checker .maybe_get_user_session(&http_request, &server_state.mysql_pool) .await .map_err(|e| { warn!("Session checker error: {:?}", e); - GenerateKling16VideoError::ServerError + CommonWebError::ServerError })?; let maybe_avt_token = server_state @@ -140,19 +102,19 @@ pub async fn generate_kling_1_6_pro_video_handler( Some(token) => token, None => { warn!("No media file token provided"); - return Err(GenerateKling16VideoError::BadInput("No media file token provided".to_string())); + return Err(CommonWebError::BadInputWithSimpleMessage("No media file token provided".to_string())); } }; if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) { - return Err(GenerateKling16VideoError::BadInput(reason)); + return Err(CommonWebError::BadInputWithSimpleMessage(reason)); } insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool) .await .map_err(|err| { error!("Error inserting idempotency token: {:?}", err); - GenerateKling16VideoError::BadInput("invalid idempotency token".to_string()) + CommonWebError::BadInputWithSimpleMessage("invalid idempotency token".to_string()) })?; const IS_MOD : bool = false; @@ -166,16 +128,16 @@ pub async fn generate_kling_1_6_pro_video_handler( Ok(Some(media_file)) => media_file, Ok(None) => { warn!("MediaFile not found: {:?}", media_file_token); - return Err(GenerateKling16VideoError::NotFound); + return Err(CommonWebError::NotFound); }, Err(err) => { warn!("Error looking up media_file: {:?}", err); - return Err(GenerateKling16VideoError::ServerError); + return Err(CommonWebError::ServerError); } }; if !media_file.media_type.is_jpg_or_png_or_legacy_image() { - return Err(GenerateKling16VideoError::BadInput("Media file must be a JPG or PNG image".to_string())); + return Err(CommonWebError::BadInputWithSimpleMessage("Media file must be a JPG or PNG image".to_string())); } let media_domain = get_media_domain(&http_request); @@ -204,12 +166,18 @@ pub async fn generate_kling_1_6_pro_video_handler( None => Kling16ProAspectRatio::WideSixteenNine, // Default to 16:9 }; + let duration = match &request.duration { + Some(GenerateKling16ProDuration::FiveSeconds) => Kling16Duration::FiveSeconds, + Some(GenerateKling16ProDuration::TenSeconds) => Kling16Duration::TenSeconds, + None => Kling16Duration::FiveSeconds, + }; + let args = Kling16ProArgs { image_url: media_links.cdn_url, webhook_url: &server_state.fal.webhook_url, - duration: Kling16Duration::Default, - prompt: prompt, - aspect_ratio: aspect_ratio, + duration, + prompt, + aspect_ratio, api_key: &server_state.fal.api_key, }; @@ -217,13 +185,13 @@ pub async fn generate_kling_1_6_pro_video_handler( .await .map_err(|err| { warn!("Error calling remove_background_rembg_webhook: {:?}", err); - GenerateKling16VideoError::ServerError + CommonWebError::ServerError })?; let external_job_id = fal_result.request_id .ok_or_else(|| { warn!("Fal request_id is None"); - GenerateKling16VideoError::ServerError + CommonWebError::ServerError })?; info!("Fal request_id: {}", external_job_id); @@ -246,7 +214,7 @@ pub async fn generate_kling_1_6_pro_video_handler( Ok(token) => token, Err(err) => { warn!("Error inserting generic inference job for FAL queue: {:?}", err); - return Err(GenerateKling16VideoError::ServerError); + return Err(CommonWebError::ServerError); } }; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/generate_kling_2_1_master_video_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/generate_kling_2_1_master_video_handler.rs new file mode 100644 index 0000000000..5849de71c8 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/generate_kling_2_1_master_video_handler.rs @@ -0,0 +1,226 @@ +use std::fmt; +use std::sync::Arc; + +use crate::http_server::common_requests::media_file_token_path_info::MediaFileTokenPathInfo; +use crate::http_server::common_responses::common_web_error::CommonWebError; +use crate::http_server::common_responses::media::media_links::MediaLinks; +use crate::http_server::deprecated_endpoints::engine::create_scene_handler::CreateSceneError; +use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain; +use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError; +use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format; +use crate::http_server::web_utils::response_success_helpers::simple_json_success; +use crate::state::server_state::ServerState; +use crate::util::delete_role_disambiguation::{delete_role_disambiguation, DeleteRole}; +use actix_web::error::ResponseError; +use actix_web::http::StatusCode; +use actix_web::web::Json; +use actix_web::web::Path; +use actix_web::{web, HttpRequest, HttpResponse}; +use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::{GenerateKling21MasterAspectRatio, GenerateKling21MasterImageToVideoRequest}; +use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::{GenerateKling21MasterDuration, GenerateKling21MasterImageToVideoResponse}; +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_21_master_image_to_video_webhook::enqueue_kling_21_master_image_to_video_webhook; +use fal_client::requests::webhook::video::enqueue_kling_21_master_image_to_video_webhook::Kling21MasterArgs; +use fal_client::requests::webhook::video::enqueue_kling_21_master_image_to_video_webhook::Kling21MasterAspectRatio; +use fal_client::requests::webhook::video::enqueue_kling_21_master_image_to_video_webhook::Kling21MasterDuration; +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; + +/* + 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, + +*/ + +/// Kling 2.1 Master Image to Video +#[utoipa::path( + post, + tag = "Generate Videos", + path = "/v1/generate/video/kling_2.1_master_image_to_video", + responses( + (status = 200, description = "Success", body = GenerateKling21MasterImageToVideoResponse), + ), + params( + ("request" = GenerateKling21MasterImageToVideoRequest, description = "Payload for Request"), + ) +)] +pub async fn generate_kling_2_1_master_video_handler( + http_request: HttpRequest, + request: Json, + server_state: web::Data> +) -> Result, CommonWebError> { + let maybe_user_session = server_state + .session_checker + .maybe_get_user_session(&http_request, &server_state.mysql_pool) + .await + .map_err(|e| { + warn!("Session checker error: {:?}", e); + CommonWebError::ServerError + })?; + + let maybe_avt_token = server_state + .avt_cookie_manager + .get_avt_token_from_request(&http_request); + + // TODO: Limit usage for new accounts. Billing, free credits metering, etc. + + //let user_session = match maybe_user_session { + // Some(session) => session, + // None => { + // warn!("not logged in"); + // return Err(RemoveImageBackgroundError::NotAuthorized); + // } + //}; + + let media_file_token = match &request.media_file_token { + Some(token) => token, + None => { + warn!("No media file token provided"); + return Err(CommonWebError::BadInputWithSimpleMessage("No media file token provided".to_string())); + } + }; + + if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) { + return Err(CommonWebError::BadInputWithSimpleMessage(reason)); + } + + insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool) + .await + .map_err(|err| { + error!("Error inserting idempotency token: {:?}", err); + CommonWebError::BadInputWithSimpleMessage("invalid idempotency token".to_string()) + })?; + const IS_MOD : bool = false; + + let media_file_lookup_result = get_media_file( + media_file_token, + IS_MOD, + &server_state.mysql_pool, + ).await; + + let media_file = match media_file_lookup_result { + Ok(Some(media_file)) => media_file, + Ok(None) => { + warn!("MediaFile not found: {:?}", media_file_token); + return Err(CommonWebError::NotFound); + }, + Err(err) => { + warn!("Error looking up media_file: {:?}", err); + return Err(CommonWebError::ServerError); + } + }; + + if !media_file.media_type.is_jpg_or_png_or_legacy_image() { + return Err(CommonWebError::BadInputWithSimpleMessage("Media file must be a JPG or PNG image".to_string())); + } + + let media_domain = get_media_domain(&http_request); + + let bucket_path = MediaFileBucketPath::from_object_hash( + &media_file.public_bucket_directory_hash, + media_file.maybe_public_bucket_prefix.as_deref(), + media_file.maybe_public_bucket_extension.as_deref()); + + let media_links = MediaLinks::from_media_path_and_env( + media_domain, + server_state.server_environment, + &bucket_path); + + info!("Fal webhook URL: {}", server_state.fal.webhook_url); + + let prompt = request.prompt + .as_deref() + .map(|prompt| prompt.trim()) + .unwrap_or_else(|| ""); + + let aspect_ratio = match &request.aspect_ratio { + Some(GenerateKling21MasterAspectRatio::Square) => Kling21MasterAspectRatio::Square, + Some(GenerateKling21MasterAspectRatio::WideSixteenNine) => Kling21MasterAspectRatio::WideSixteenNine, + Some(GenerateKling21MasterAspectRatio::TallNineSixteen) => Kling21MasterAspectRatio::TallNineSixteen, + None => Kling21MasterAspectRatio::WideSixteenNine, // Default to 16:9 + }; + + let duration = match &request.duration { + Some(GenerateKling21MasterDuration::FiveSeconds) => Kling21MasterDuration::FiveSeconds, + Some(GenerateKling21MasterDuration::TenSeconds) => Kling21MasterDuration::TenSeconds, + None => Kling21MasterDuration::FiveSeconds, + }; + + let args = Kling21MasterArgs { + image_url: media_links.cdn_url, + webhook_url: &server_state.fal.webhook_url, + duration, + prompt, + aspect_ratio, + api_key: &server_state.fal.api_key, + }; + + let fal_result = enqueue_kling_21_master_image_to_video_webhook(args) + .await + .map_err(|err| { + warn!("Error calling enqueue_kling_21_master_image_to_video_webhook: {:?}", err); + CommonWebError::ServerError + })?; + + let external_job_id = fal_result.request_id + .ok_or_else(|| { + warn!("Fal request_id is None"); + CommonWebError::ServerError + })?; + + info!("Fal request_id: {}", external_job_id); + + let ip_address = get_request_ip(&http_request); + + let db_result = insert_generic_inference_job_for_fal_queue(InsertGenericInferenceForFalArgs { + uuid_idempotency_token: &request.uuid_idempotency_token, + maybe_external_third_party_id: &external_job_id, + fal_category: FalCategory::VideoGeneration, + maybe_inference_args: None, + maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token), + maybe_avt_token: maybe_avt_token.as_ref(), + creator_ip_address: &ip_address, + creator_set_visibility: Visibility::Public, + mysql_pool: &server_state.mysql_pool, + }).await; + + let job_token = match db_result { + Ok(token) => token, + Err(err) => { + warn!("Error inserting generic inference job for FAL queue: {:?}", err); + return Err(CommonWebError::ServerError); + } + }; + + Ok(Json(GenerateKling21MasterImageToVideoResponse { + success: true, + inference_job_token: job_token, + })) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/generate_kling_2_1_pro_video_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/generate_kling_2_1_pro_video_handler.rs new file mode 100644 index 0000000000..7870b5ba05 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/generate_kling_2_1_pro_video_handler.rs @@ -0,0 +1,226 @@ +use std::fmt; +use std::sync::Arc; + +use crate::http_server::common_requests::media_file_token_path_info::MediaFileTokenPathInfo; +use crate::http_server::common_responses::common_web_error::CommonWebError; +use crate::http_server::common_responses::media::media_links::MediaLinks; +use crate::http_server::deprecated_endpoints::engine::create_scene_handler::CreateSceneError; +use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain; +use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError; +use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format; +use crate::http_server::web_utils::response_success_helpers::simple_json_success; +use crate::state::server_state::ServerState; +use crate::util::delete_role_disambiguation::{delete_role_disambiguation, DeleteRole}; +use actix_web::error::ResponseError; +use actix_web::http::StatusCode; +use actix_web::web::Json; +use actix_web::web::Path; +use actix_web::{web, HttpRequest, HttpResponse}; +use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::{GenerateKling21ProAspectRatio, GenerateKling21ProImageToVideoRequest}; +use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::{GenerateKling21ProDuration, GenerateKling21ProImageToVideoResponse}; +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_21_pro_image_to_video_webhook::enqueue_kling_21_pro_image_to_video_webhook; +use fal_client::requests::webhook::video::enqueue_kling_21_pro_image_to_video_webhook::Kling21ProArgs; +use fal_client::requests::webhook::video::enqueue_kling_21_pro_image_to_video_webhook::Kling21ProAspectRatio; +use fal_client::requests::webhook::video::enqueue_kling_21_pro_image_to_video_webhook::Kling21ProDuration; +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; + +/* + 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, + +*/ + +/// Kling 2.1 Pro Image to Video +#[utoipa::path( + post, + tag = "Generate Videos", + path = "/v1/generate/video/kling_2.1_pro_image_to_video", + responses( + (status = 200, description = "Success", body = GenerateKling21ProImageToVideoResponse), + ), + params( + ("request" = GenerateKling21ProImageToVideoRequest, description = "Payload for Request"), + ) +)] +pub async fn generate_kling_2_1_pro_video_handler( + http_request: HttpRequest, + request: Json, + server_state: web::Data> +) -> Result, CommonWebError> { + let maybe_user_session = server_state + .session_checker + .maybe_get_user_session(&http_request, &server_state.mysql_pool) + .await + .map_err(|e| { + warn!("Session checker error: {:?}", e); + CommonWebError::ServerError + })?; + + let maybe_avt_token = server_state + .avt_cookie_manager + .get_avt_token_from_request(&http_request); + + // TODO: Limit usage for new accounts. Billing, free credits metering, etc. + + //let user_session = match maybe_user_session { + // Some(session) => session, + // None => { + // warn!("not logged in"); + // return Err(RemoveImageBackgroundError::NotAuthorized); + // } + //}; + + let media_file_token = match &request.media_file_token { + Some(token) => token, + None => { + warn!("No media file token provided"); + return Err(CommonWebError::BadInputWithSimpleMessage("No media file token provided".to_string())); + } + }; + + if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) { + return Err(CommonWebError::BadInputWithSimpleMessage(reason)); + } + + insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool) + .await + .map_err(|err| { + error!("Error inserting idempotency token: {:?}", err); + CommonWebError::BadInputWithSimpleMessage("invalid idempotency token".to_string()) + })?; + const IS_MOD : bool = false; + + let media_file_lookup_result = get_media_file( + media_file_token, + IS_MOD, + &server_state.mysql_pool, + ).await; + + let media_file = match media_file_lookup_result { + Ok(Some(media_file)) => media_file, + Ok(None) => { + warn!("MediaFile not found: {:?}", media_file_token); + return Err(CommonWebError::NotFound); + }, + Err(err) => { + warn!("Error looking up media_file: {:?}", err); + return Err(CommonWebError::ServerError); + } + }; + + if !media_file.media_type.is_jpg_or_png_or_legacy_image() { + return Err(CommonWebError::BadInputWithSimpleMessage("Media file must be a JPG or PNG image".to_string())); + } + + let media_domain = get_media_domain(&http_request); + + let bucket_path = MediaFileBucketPath::from_object_hash( + &media_file.public_bucket_directory_hash, + media_file.maybe_public_bucket_prefix.as_deref(), + media_file.maybe_public_bucket_extension.as_deref()); + + let media_links = MediaLinks::from_media_path_and_env( + media_domain, + server_state.server_environment, + &bucket_path); + + info!("Fal webhook URL: {}", server_state.fal.webhook_url); + + let prompt = request.prompt + .as_deref() + .map(|prompt| prompt.trim()) + .unwrap_or_else(|| ""); + + let aspect_ratio = match &request.aspect_ratio { + Some(GenerateKling21ProAspectRatio::Square) => Kling21ProAspectRatio::Square, + Some(GenerateKling21ProAspectRatio::WideSixteenNine) => Kling21ProAspectRatio::WideSixteenNine, + Some(GenerateKling21ProAspectRatio::TallNineSixteen) => Kling21ProAspectRatio::TallNineSixteen, + None => Kling21ProAspectRatio::WideSixteenNine, // Default to 16:9 + }; + + let duration = match &request.duration { + Some(GenerateKling21ProDuration::FiveSeconds) => Kling21ProDuration::FiveSeconds, + Some(GenerateKling21ProDuration::TenSeconds) => Kling21ProDuration::TenSeconds, + None => Kling21ProDuration::FiveSeconds, + }; + + let args = Kling21ProArgs { + image_url: media_links.cdn_url, + webhook_url: &server_state.fal.webhook_url, + duration, + prompt, + aspect_ratio, + api_key: &server_state.fal.api_key, + }; + + let fal_result = enqueue_kling_21_pro_image_to_video_webhook(args) + .await + .map_err(|err| { + warn!("Error calling enqueue_kling_21_pro_image_to_video_webhook: {:?}", err); + CommonWebError::ServerError + })?; + + let external_job_id = fal_result.request_id + .ok_or_else(|| { + warn!("Fal request_id is None"); + CommonWebError::ServerError + })?; + + info!("Fal request_id: {}", external_job_id); + + let ip_address = get_request_ip(&http_request); + + let db_result = insert_generic_inference_job_for_fal_queue(InsertGenericInferenceForFalArgs { + uuid_idempotency_token: &request.uuid_idempotency_token, + maybe_external_third_party_id: &external_job_id, + fal_category: FalCategory::VideoGeneration, + maybe_inference_args: None, + maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token), + maybe_avt_token: maybe_avt_token.as_ref(), + creator_ip_address: &ip_address, + creator_set_visibility: Visibility::Public, + mysql_pool: &server_state.mysql_pool, + }).await; + + let job_token = match db_result { + Ok(token) => token, + Err(err) => { + warn!("Error inserting generic inference job for FAL queue: {:?}", err); + return Err(CommonWebError::ServerError); + } + }; + + Ok(Json(GenerateKling21ProImageToVideoResponse { + success: true, + inference_job_token: job_token, + })) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/generate_seedance_1_0_lite_image_to_video_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/generate_seedance_1_0_lite_image_to_video_handler.rs new file mode 100644 index 0000000000..d557243495 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/generate_seedance_1_0_lite_image_to_video_handler.rs @@ -0,0 +1,205 @@ +use std::fmt; +use std::sync::Arc; + +use crate::http_server::common_requests::media_file_token_path_info::MediaFileTokenPathInfo; +use crate::http_server::common_responses::common_web_error::CommonWebError; +use crate::http_server::common_responses::media::media_links::MediaLinks; +use crate::http_server::deprecated_endpoints::engine::create_scene_handler::CreateSceneError; +use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain; +use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError; +use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format; +use crate::http_server::web_utils::response_success_helpers::simple_json_success; +use crate::state::server_state::ServerState; +use crate::util::delete_role_disambiguation::{delete_role_disambiguation, DeleteRole}; +use actix_web::error::ResponseError; +use actix_web::http::StatusCode; +use actix_web::web::Json; +use actix_web::web::Path; +use actix_web::{web, HttpRequest, HttpResponse}; +use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteDuration; +use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteImageToVideoRequest; +use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteImageToVideoResponse; +use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteResolution; +use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath; +use enums::common::visibility::Visibility; +use fal_client::requests::webhook::video::enqueue_seedance_1_lite_image_to_video_webhook::enqueue_seedance_1_lite_image_to_video_webhook; +use fal_client::requests::webhook::video::enqueue_seedance_1_lite_image_to_video_webhook::Seedance1LiteDuration; +use fal_client::requests::webhook::video::enqueue_seedance_1_lite_image_to_video_webhook::{Seedance1LiteArgs, Seedance1LiteResolution}; +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; + +/// Seedance 1.0 Lite Image to Video +#[utoipa::path( + post, + tag = "Generate Videos", + path = "/v1/generate/video/seedance_1.0_lite_image_to_video", + responses( + (status = 200, description = "Success", body = GenerateSeedance10LiteImageToVideoResponse), + ), + params( + ("request" = GenerateSeedance10LiteImageToVideoRequest, description = "Payload for Request"), + ) +)] +pub async fn generate_seedance_1_0_lite_image_to_video_handler( + http_request: HttpRequest, + request: Json, + server_state: web::Data> +) -> Result, CommonWebError> { + let maybe_user_session = server_state + .session_checker + .maybe_get_user_session(&http_request, &server_state.mysql_pool) + .await + .map_err(|e| { + warn!("Session checker error: {:?}", e); + CommonWebError::ServerError + })?; + + let maybe_avt_token = server_state + .avt_cookie_manager + .get_avt_token_from_request(&http_request); + + // TODO: Limit usage for new accounts. Billing, free credits metering, etc. + + //let user_session = match maybe_user_session { + // Some(session) => session, + // None => { + // warn!("not logged in"); + // return Err(RemoveImageBackgroundError::NotAuthorized); + // } + //}; + + let media_file_token = match &request.media_file_token { + Some(token) => token, + None => { + warn!("No media file token provided"); + return Err(CommonWebError::BadInputWithSimpleMessage("No media file token provided".to_string())); + } + }; + + if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) { + return Err(CommonWebError::BadInputWithSimpleMessage(reason)); + } + + insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool) + .await + .map_err(|err| { + error!("Error inserting idempotency token: {:?}", err); + CommonWebError::BadInputWithSimpleMessage("invalid idempotency token".to_string()) + })?; + const IS_MOD : bool = false; + + let media_file_lookup_result = get_media_file( + media_file_token, + IS_MOD, + &server_state.mysql_pool, + ).await; + + let media_file = match media_file_lookup_result { + Ok(Some(media_file)) => media_file, + Ok(None) => { + warn!("MediaFile not found: {:?}", media_file_token); + return Err(CommonWebError::NotFound); + }, + Err(err) => { + warn!("Error looking up media_file: {:?}", err); + return Err(CommonWebError::ServerError); + } + }; + + if !media_file.media_type.is_jpg_or_png_or_legacy_image() { + return Err(CommonWebError::BadInputWithSimpleMessage("Media file must be a JPG or PNG image".to_string())); + } + + let media_domain = get_media_domain(&http_request); + + let bucket_path = MediaFileBucketPath::from_object_hash( + &media_file.public_bucket_directory_hash, + media_file.maybe_public_bucket_prefix.as_deref(), + media_file.maybe_public_bucket_extension.as_deref()); + + let media_links = MediaLinks::from_media_path_and_env( + media_domain, + server_state.server_environment, + &bucket_path); + + info!("Fal webhook URL: {}", server_state.fal.webhook_url); + + let prompt = request.prompt + .as_deref() + .map(|prompt| prompt.trim()) + .unwrap_or_else(|| ""); + + let resolution = match &request.resolution { + Some(GenerateSeedance10LiteResolution::FourEightyP) => Seedance1LiteResolution::FourEightyP, + Some(GenerateSeedance10LiteResolution::SevenTwentyP) => Seedance1LiteResolution::SevenTwentyP, + None => Seedance1LiteResolution::SevenTwentyP, + }; + + let duration = match &request.duration { + Some(GenerateSeedance10LiteDuration::FiveSeconds) => Seedance1LiteDuration::FiveSeconds, + Some(GenerateSeedance10LiteDuration::TenSeconds) => Seedance1LiteDuration::TenSeconds, + None => Seedance1LiteDuration::FiveSeconds, + }; + + let args = Seedance1LiteArgs { + image_url: media_links.cdn_url, + webhook_url: &server_state.fal.webhook_url, + api_key: &server_state.fal.api_key, + duration, + resolution, + prompt, + camera_fixed: false, // TODO: Parameterize + seed: None, // TODO: Parameterize + }; + + let fal_result = enqueue_seedance_1_lite_image_to_video_webhook(args) + .await + .map_err(|err| { + warn!("Error calling enqueue_seedance_1_lite_image_to_video_webhook: {:?}", err); + CommonWebError::ServerError + })?; + + let external_job_id = fal_result.request_id + .ok_or_else(|| { + warn!("Fal request_id is None"); + CommonWebError::ServerError + })?; + + info!("Fal request_id: {}", external_job_id); + + let ip_address = get_request_ip(&http_request); + + let db_result = insert_generic_inference_job_for_fal_queue(InsertGenericInferenceForFalArgs { + uuid_idempotency_token: &request.uuid_idempotency_token, + maybe_external_third_party_id: &external_job_id, + fal_category: FalCategory::VideoGeneration, + maybe_inference_args: None, + maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token), + maybe_avt_token: maybe_avt_token.as_ref(), + creator_ip_address: &ip_address, + creator_set_visibility: Visibility::Public, + mysql_pool: &server_state.mysql_pool, + }).await; + + let job_token = match db_result { + Ok(token) => token, + Err(err) => { + warn!("Error inserting generic inference job for FAL queue: {:?}", err); + return Err(CommonWebError::ServerError); + } + }; + + Ok(Json(GenerateSeedance10LiteImageToVideoResponse { + success: true, + inference_job_token: job_token, + })) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/generate_veo_2_image_to_video_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/generate_veo_2_image_to_video_handler.rs new file mode 100644 index 0000000000..96a5f3b828 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/generate_veo_2_image_to_video_handler.rs @@ -0,0 +1,208 @@ +use std::fmt; +use std::sync::Arc; + +use crate::http_server::common_requests::media_file_token_path_info::MediaFileTokenPathInfo; +use crate::http_server::common_responses::common_web_error::CommonWebError; +use crate::http_server::common_responses::media::media_links::MediaLinks; +use crate::http_server::deprecated_endpoints::engine::create_scene_handler::CreateSceneError; +use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain; +use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError; +use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format; +use crate::http_server::web_utils::response_success_helpers::simple_json_success; +use crate::state::server_state::ServerState; +use crate::util::delete_role_disambiguation::{delete_role_disambiguation, DeleteRole}; +use actix_web::error::ResponseError; +use actix_web::http::StatusCode; +use actix_web::web::Json; +use actix_web::web::Path; +use actix_web::{web, HttpRequest, HttpResponse}; +use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2AspectRatio; +use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2Duration; +use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2ImageToVideoRequest; +use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2ImageToVideoResponse; +use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath; +use enums::common::visibility::Visibility; +use fal_client::requests::webhook::video::enqueue_veo_2_image_to_video_webhook::enqueue_veo_2_image_to_video_webhook; +use fal_client::requests::webhook::video::enqueue_veo_2_image_to_video_webhook::Veo2Args; +use fal_client::requests::webhook::video::enqueue_veo_2_image_to_video_webhook::Veo2AspectRatio; +use fal_client::requests::webhook::video::enqueue_veo_2_image_to_video_webhook::Veo2Duration; +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; + +/// Veo 2 Image to Video +#[utoipa::path( + post, + tag = "Generate Videos", + path = "/v1/generate/video/veo_2_image_to_video", + responses( + (status = 200, description = "Success", body = GenerateVeo2ImageToVideoResponse), + ), + params( + ("request" = GenerateVeo2ImageToVideoRequest, description = "Payload for Request"), + ) +)] +pub async fn generate_veo_2_image_to_video_handler( + http_request: HttpRequest, + request: Json, + server_state: web::Data> +) -> Result, CommonWebError> { + let maybe_user_session = server_state + .session_checker + .maybe_get_user_session(&http_request, &server_state.mysql_pool) + .await + .map_err(|e| { + warn!("Session checker error: {:?}", e); + CommonWebError::ServerError + })?; + + let maybe_avt_token = server_state + .avt_cookie_manager + .get_avt_token_from_request(&http_request); + + // TODO: Limit usage for new accounts. Billing, free credits metering, etc. + + //let user_session = match maybe_user_session { + // Some(session) => session, + // None => { + // warn!("not logged in"); + // return Err(RemoveImageBackgroundError::NotAuthorized); + // } + //}; + + let media_file_token = match &request.media_file_token { + Some(token) => token, + None => { + warn!("No media file token provided"); + return Err(CommonWebError::BadInputWithSimpleMessage("No media file token provided".to_string())); + } + }; + + if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) { + return Err(CommonWebError::BadInputWithSimpleMessage(reason)); + } + + insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool) + .await + .map_err(|err| { + error!("Error inserting idempotency token: {:?}", err); + CommonWebError::BadInputWithSimpleMessage("invalid idempotency token".to_string()) + })?; + const IS_MOD : bool = false; + + let media_file_lookup_result = get_media_file( + media_file_token, + IS_MOD, + &server_state.mysql_pool, + ).await; + + let media_file = match media_file_lookup_result { + Ok(Some(media_file)) => media_file, + Ok(None) => { + warn!("MediaFile not found: {:?}", media_file_token); + return Err(CommonWebError::NotFound); + }, + Err(err) => { + warn!("Error looking up media_file: {:?}", err); + return Err(CommonWebError::ServerError); + } + }; + + if !media_file.media_type.is_jpg_or_png_or_legacy_image() { + return Err(CommonWebError::BadInputWithSimpleMessage("Media file must be a JPG or PNG image".to_string())); + } + + let media_domain = get_media_domain(&http_request); + + let bucket_path = MediaFileBucketPath::from_object_hash( + &media_file.public_bucket_directory_hash, + media_file.maybe_public_bucket_prefix.as_deref(), + media_file.maybe_public_bucket_extension.as_deref()); + + let media_links = MediaLinks::from_media_path_and_env( + media_domain, + server_state.server_environment, + &bucket_path); + + info!("Fal webhook URL: {}", server_state.fal.webhook_url); + + let prompt = request.prompt + .as_deref() + .map(|prompt| prompt.trim()) + .unwrap_or_else(|| ""); + + let aspect_ratio = match &request.aspect_ratio { + Some(GenerateVeo2AspectRatio::Auto) => Veo2AspectRatio::Auto, + Some(GenerateVeo2AspectRatio::AutoPreferPortrait) => Veo2AspectRatio::AutoPreferPortrait, + Some(GenerateVeo2AspectRatio::WideSixteenNine) => Veo2AspectRatio::WideSixteenNine, + Some(GenerateVeo2AspectRatio::TallNineSixteen) => Veo2AspectRatio::TallNineSixteen, + None => Veo2AspectRatio::WideSixteenNine, // Default to 16:9 + }; + + let duration = match &request.duration { + Some(GenerateVeo2Duration::FiveSeconds) => Veo2Duration::FiveSeconds, + Some(GenerateVeo2Duration::SixSeconds) => Veo2Duration::SixSeconds, + Some(GenerateVeo2Duration::SevenSeconds) => Veo2Duration::SevenSeconds, + Some(GenerateVeo2Duration::EightSeconds) => Veo2Duration::EightSeconds, + None => Veo2Duration::FiveSeconds, + }; + + let args = Veo2Args { + image_url: media_links.cdn_url, + webhook_url: &server_state.fal.webhook_url, + duration, + prompt, + aspect_ratio, + api_key: &server_state.fal.api_key, + }; + + let fal_result = enqueue_veo_2_image_to_video_webhook(args) + .await + .map_err(|err| { + warn!("Error calling enqueue_veo_2_image_to_video_webhook: {:?}", err); + CommonWebError::ServerError + })?; + + let external_job_id = fal_result.request_id + .ok_or_else(|| { + warn!("Fal request_id is None"); + CommonWebError::ServerError + })?; + + info!("Fal request_id: {}", external_job_id); + + let ip_address = get_request_ip(&http_request); + + let db_result = insert_generic_inference_job_for_fal_queue(InsertGenericInferenceForFalArgs { + uuid_idempotency_token: &request.uuid_idempotency_token, + maybe_external_third_party_id: &external_job_id, + fal_category: FalCategory::VideoGeneration, + maybe_inference_args: None, + maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token), + maybe_avt_token: maybe_avt_token.as_ref(), + creator_ip_address: &ip_address, + creator_set_visibility: Visibility::Public, + mysql_pool: &server_state.mysql_pool, + }).await; + + let job_token = match db_result { + Ok(token) => token, + Err(err) => { + warn!("Error inserting generic inference job for FAL queue: {:?}", err); + return Err(CommonWebError::ServerError); + } + }; + + Ok(Json(GenerateVeo2ImageToVideoResponse { + success: true, + inference_job_token: job_token, + })) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/mod.rs index d7a0e16804..0dacc10a6c 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/mod.rs @@ -1 +1,5 @@ -pub mod generate_kling_1_6_pro_video_handler; \ No newline at end of file +pub mod generate_kling_1_6_pro_video_handler; +pub mod generate_kling_2_1_master_video_handler; +pub mod generate_kling_2_1_pro_video_handler; +pub mod generate_seedance_1_0_lite_image_to_video_handler; +pub mod generate_veo_2_image_to_video_handler; \ No newline at end of file diff --git a/crates/service/web/storyteller_web/src/http_server/routes/add_generate_routes.rs b/crates/service/web/storyteller_web/src/http_server/routes/add_generate_routes.rs index 4ab6f6bbcc..3c2ca01780 100644 --- a/crates/service/web/storyteller_web/src/http_server/routes/add_generate_routes.rs +++ b/crates/service/web/storyteller_web/src/http_server/routes/add_generate_routes.rs @@ -1,7 +1,15 @@ -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::generate_flux_1_dev_text_to_image_handler::generate_flux_1_dev_text_to_image_handler; +use crate::http_server::endpoints::generate::image::generate_flux_1_schnell_text_to_image_handler::generate_flux_1_schnell_text_to_image_handler; +use crate::http_server::endpoints::generate::image::generate_flux_pro_11_text_to_image_handler::generate_flux_pro_11_text_to_image_handler; +use crate::http_server::endpoints::generate::image::generate_flux_pro_11_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::object::generate_hunyuan_21_image_to_3d_handler::generate_hunyuan_21_image_to_3d_handler; +use crate::http_server::endpoints::generate::object::generate_hunyuan_2_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_2_1_master_video_handler::generate_kling_2_1_master_video_handler; +use crate::http_server::endpoints::generate::video::generate_kling_2_1_pro_video_handler::generate_kling_2_1_pro_video_handler; +use crate::http_server::endpoints::generate::video::generate_seedance_1_0_lite_image_to_video_handler::generate_seedance_1_0_lite_image_to_video_handler; +use crate::http_server::endpoints::generate::video::generate_veo_2_image_to_video_handler::generate_veo_2_image_to_video_handler; use actix_http::body::MessageBody; use actix_service::ServiceFactory; use actix_web::dev::{ServiceRequest, ServiceResponse}; @@ -20,26 +28,58 @@ 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::resource("/flux_1_dev_text_to_image") + .route(web::post().to(generate_flux_1_dev_text_to_image_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) + .service(web::resource("/flux_1_schnell_text_to_image") + .route(web::post().to(generate_flux_1_schnell_text_to_image_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) + .service(web::resource("/flux_pro_1.1_text_to_image") + .route(web::post().to(generate_flux_pro_11_text_to_image_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) + .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)) .route(web::head().to(|| HttpResponse::Ok())) ) + .service(web::resource("/kling_2.1_master_image_to_video") + .route(web::post().to(generate_kling_2_1_master_video_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) + .service(web::resource("/kling_2.1_pro_image_to_video") + .route(web::post().to(generate_kling_2_1_pro_video_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) + .service(web::resource("/seedance_1.0_lite_image_to_video") + .route(web::post().to(generate_seedance_1_0_lite_image_to_video_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) + .service(web::resource("/veo_2_image_to_video") + .route(web::post().to(generate_veo_2_image_to_video_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) ) .service(web::scope("/object") .service(web::resource("/hunyuan_2_image_to_3d") .route(web::post().to(generate_hunyuan_2_image_to_3d_handler)) .route(web::head().to(|| HttpResponse::Ok())) ) + .service(web::resource("/hunyuan_2.1_image_to_3d") + .route(web::post().to(generate_hunyuan_21_image_to_3d_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) ) ) }