mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-24 02:29:32 -04:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
617aa75dc5 | ||
|
|
10603f7c86 | ||
|
|
5e8c524f84 | ||
|
|
05f0fe1216 | ||
|
|
1f44f7e929 |
Generated
+1642
-1961
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -2,4 +2,6 @@ pub mod fal_background_removal_command;
|
||||
pub mod fal_hunyuan_image_to_3d_command;
|
||||
pub mod fal_kling_image_to_video_command;
|
||||
pub mod get_fal_api_key_command;
|
||||
pub mod set_fal_api_key_command;
|
||||
pub mod set_fal_api_key_command;
|
||||
// pub mod get_open_ai_api_key_command;
|
||||
// pub mod set_open_ai_api_key_command;
|
||||
+38
-3
@@ -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<U: IntoUrl>(
|
||||
args: FluxPro11UltraArgs<'_, U>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
let num_images = match args.num_images {
|
||||
FluxPro11UltraNumImages::One => 1,
|
||||
FluxPro11UltraNumImages::Two => 2,
|
||||
FluxPro11UltraNumImages::Three => 3,
|
||||
FluxPro11UltraNumImages::Four => 4,
|
||||
};
|
||||
|
||||
let aspect_ratio = match args.aspect_ratio {
|
||||
FluxPro11UltraAspectRatio::Square => AspectRatioProperty::Property_1_1,
|
||||
FluxPro11UltraAspectRatio::LandscapeFourByThree => AspectRatioProperty::Property_4_3,
|
||||
FluxPro11UltraAspectRatio::LandscapeSixteenByNine => AspectRatioProperty::Property_16_9,
|
||||
FluxPro11UltraAspectRatio::PortraitThreeByFour => AspectRatioProperty::Property_3_4,
|
||||
FluxPro11UltraAspectRatio::PortraitNineBySixteen => AspectRatioProperty::Property_9_16,
|
||||
};
|
||||
|
||||
let request = FluxProUltraTextToImageInput {
|
||||
prompt: args.prompt.to_string(),
|
||||
num_images: Some(num_images),
|
||||
aspect_ratio: Some(aspect_ratio),
|
||||
// Maybe expose
|
||||
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
|
||||
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub const GENERATE_FLUX_1_DEV_TEXT_TO_IMAGE_PATH: &str = "/v1/generate/image/flux_1_dev_text_to_image";
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct GenerateFlux1DevTextToImageRequest {
|
||||
/// Idempotency token to prevent duplicate requests.
|
||||
pub uuid_idempotency_token: String,
|
||||
|
||||
/// Text prompt to generate the image from.
|
||||
pub prompt: Option<String>,
|
||||
|
||||
/// Aspect ratio of the output images.
|
||||
pub aspect_ratio: Option<GenerateFlux1DevTextToImageAspectRatio>,
|
||||
|
||||
/// Number of images to generate. Default is one.
|
||||
pub num_images: Option<GenerateFlux1DevTextToImageNumImages>,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateFlux1DevTextToImageAspectRatio {
|
||||
Square, // 1:1
|
||||
SquareHd, // 1:1
|
||||
LandscapeFourByThree, // 4:3
|
||||
LandscapeSixteenByNine, // 16:9
|
||||
PortraitThreeByFour, // 3:4
|
||||
PortraitNineBySixteen, // 9:16
|
||||
//Custom { width: u32, height: u32 }, // TODO
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateFlux1DevTextToImageNumImages {
|
||||
One, // Default
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct GenerateFlux1DevTextToImageResponse {
|
||||
pub success: bool,
|
||||
pub inference_job_token: InferenceJobToken,
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub const GENERATE_FLUX_1_SCHNELL_TEXT_TO_IMAGE_PATH: &str = "/v1/generate/image/flux_1_schnell_text_to_image";
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct GenerateFlux1SchnellTextToImageRequest {
|
||||
/// Idempotency token to prevent duplicate requests.
|
||||
pub uuid_idempotency_token: String,
|
||||
|
||||
/// Text prompt to generate the image from.
|
||||
pub prompt: Option<String>,
|
||||
|
||||
/// Aspect ratio of the output images.
|
||||
pub aspect_ratio: Option<GenerateFlux1SchnellTextToImageAspectRatio>,
|
||||
|
||||
/// Number of images to generate. Default is one.
|
||||
pub num_images: Option<GenerateFlux1SchnellTextToImageNumImages>,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateFlux1SchnellTextToImageAspectRatio {
|
||||
Square, // 1:1
|
||||
SquareHd, // 1:1
|
||||
LandscapeFourByThree, // 4:3
|
||||
LandscapeSixteenByNine, // 16:9
|
||||
PortraitThreeByFour, // 3:4
|
||||
PortraitNineBySixteen, // 9:16
|
||||
//Custom { width: u32, height: u32 }, // TODO
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateFlux1SchnellTextToImageNumImages {
|
||||
One, // Default
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct GenerateFlux1SchnellTextToImageResponse {
|
||||
pub success: bool,
|
||||
pub inference_job_token: InferenceJobToken,
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub const GENERATE_FLUX_PRO_11_TEXT_TO_IMAGE_PATH: &str = "/v1/generate/image/flux_pro_1.1_text_to_image";
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct GenerateFluxPro11TextToImageRequest {
|
||||
/// Idempotency token to prevent duplicate requests.
|
||||
pub uuid_idempotency_token: String,
|
||||
|
||||
/// Text prompt to generate the image from.
|
||||
pub prompt: Option<String>,
|
||||
|
||||
/// Aspect ratio of the output images.
|
||||
pub aspect_ratio: Option<GenerateFluxPro11TextToImageAspectRatio>,
|
||||
|
||||
/// Number of images to generate. Default is one.
|
||||
pub num_images: Option<GenerateFluxPro11TextToImageNumImages>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateFluxPro11TextToImageAspectRatio {
|
||||
Square, // 1:1
|
||||
LandscapeFourByThree, // 4:3
|
||||
LandscapeSixteenByNine, // 16:9
|
||||
PortraitThreeByFour, // 3:4
|
||||
PortraitNineBySixteen, // 9:16
|
||||
//Custom { width: u32, height: u32 }, // TODO
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateFluxPro11TextToImageNumImages {
|
||||
One, // Default
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct GenerateFluxPro11TextToImageResponse {
|
||||
pub success: bool,
|
||||
pub inference_job_token: InferenceJobToken,
|
||||
}
|
||||
+26
@@ -11,6 +11,32 @@ pub struct GenerateFluxPro11UltraTextToImageRequest {
|
||||
|
||||
/// Text prompt to generate the image from.
|
||||
pub prompt: Option<String>,
|
||||
|
||||
/// Aspect ratio of the output images.
|
||||
pub aspect_ratio: Option<GenerateFluxPro11UltraTextToImageAspectRatio>,
|
||||
|
||||
/// Number of images to generate. Default is one.
|
||||
pub num_images: Option<GenerateFluxPro11UltraTextToImageNumImages>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateFluxPro11UltraTextToImageAspectRatio {
|
||||
Square, // 1:1
|
||||
LandscapeFourByThree, // 4:3
|
||||
LandscapeSixteenByNine, // 16:9
|
||||
PortraitThreeByFour, // 3:4
|
||||
PortraitNineBySixteen, // 9:16
|
||||
//Custom { width: u32, height: u32 }, // TODO
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateFluxPro11UltraTextToImageNumImages {
|
||||
One, // Default
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
|
||||
@@ -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;
|
||||
+22
@@ -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<MediaFileToken>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct GenerateHunyuan21ImageTo3dResponse {
|
||||
pub success: bool,
|
||||
pub inference_job_token: InferenceJobToken,
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
pub mod generate_hunyuan_21_image_to_3d;
|
||||
pub mod generate_hunyuan_2_image_to_3d;
|
||||
+12
-1
@@ -17,11 +17,15 @@ pub struct GenerateKling16ProImageToVideoRequest {
|
||||
/// Optional text prompt.
|
||||
pub prompt: Option<String>,
|
||||
|
||||
/// Optional: apsect ratio of the generated video.
|
||||
/// Optional: aspect ratio of the generated video.
|
||||
pub aspect_ratio: Option<GenerateKling16ProAspectRatio>,
|
||||
|
||||
/// Optional: aspect ratio of the generated video.
|
||||
pub duration: Option<GenerateKling16ProDuration>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateKling16ProAspectRatio {
|
||||
/// 16:9 aspect ratio
|
||||
WideSixteenNine,
|
||||
@@ -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,
|
||||
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub const GENERATE_KLING_2_1_MASTER_IMAGE_TO_VIDEO_URL_PATH: &str = "/v1/generate/video/kling_2.1_master_image_to_video";
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct GenerateKling21MasterImageToVideoRequest {
|
||||
/// Idempotency token to prevent duplicate requests.
|
||||
pub uuid_idempotency_token: String,
|
||||
|
||||
/// Source of the image file to convert to video.
|
||||
/// It must be an image.
|
||||
pub media_file_token: Option<MediaFileToken>,
|
||||
|
||||
/// Optional text prompt.
|
||||
pub prompt: Option<String>,
|
||||
|
||||
/// Optional: aspect ratio of the generated video.
|
||||
pub aspect_ratio: Option<GenerateKling21MasterAspectRatio>,
|
||||
|
||||
/// Optional: aspect ratio of the generated video.
|
||||
pub duration: Option<GenerateKling21MasterDuration>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateKling21MasterAspectRatio {
|
||||
/// 16:9 aspect ratio
|
||||
WideSixteenNine,
|
||||
/// 9:16 aspect ratio
|
||||
TallNineSixteen,
|
||||
/// 1:1 aspect ratio
|
||||
Square,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateKling21MasterDuration {
|
||||
FiveSeconds,
|
||||
TenSeconds,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct GenerateKling21MasterImageToVideoResponse {
|
||||
pub success: bool,
|
||||
pub inference_job_token: InferenceJobToken,
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub const GENERATE_KLING_2_1_PRO_IMAGE_TO_VIDEO_URL_PATH: &str = "/v1/generate/video/kling_2.1_pro_image_to_video";
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct GenerateKling21ProImageToVideoRequest {
|
||||
/// Idempotency token to prevent duplicate requests.
|
||||
pub uuid_idempotency_token: String,
|
||||
|
||||
/// Source of the image file to convert to video.
|
||||
/// It must be an image.
|
||||
pub media_file_token: Option<MediaFileToken>,
|
||||
|
||||
/// Optional text prompt.
|
||||
pub prompt: Option<String>,
|
||||
|
||||
/// Optional: aspect ratio of the generated video.
|
||||
pub aspect_ratio: Option<GenerateKling21ProAspectRatio>,
|
||||
|
||||
/// Optional: aspect ratio of the generated video.
|
||||
pub duration: Option<GenerateKling21ProDuration>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateKling21ProAspectRatio {
|
||||
/// 16:9 aspect ratio
|
||||
WideSixteenNine,
|
||||
/// 9:16 aspect ratio
|
||||
TallNineSixteen,
|
||||
/// 1:1 aspect ratio
|
||||
Square,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateKling21ProDuration {
|
||||
FiveSeconds,
|
||||
TenSeconds,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct GenerateKling21ProImageToVideoResponse {
|
||||
pub success: bool,
|
||||
pub inference_job_token: InferenceJobToken,
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub const GENERATE_SEEDANCE_1_0_LITE_IMAGE_TO_VIDEO_URL_PATH: &str = "/v1/generate/video/seedance_1.0_lite_image_to_video";
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct GenerateSeedance10LiteImageToVideoRequest {
|
||||
/// Idempotency token to prevent duplicate requests.
|
||||
pub uuid_idempotency_token: String,
|
||||
|
||||
/// Source of the image file to convert to video.
|
||||
/// It must be an image.
|
||||
pub media_file_token: Option<MediaFileToken>,
|
||||
|
||||
/// Optional text prompt.
|
||||
pub prompt: Option<String>,
|
||||
|
||||
/// Optional: aspect ratio of the generated video.
|
||||
pub resolution: Option<GenerateSeedance10LiteResolution>,
|
||||
|
||||
/// Optional: aspect ratio of the generated video.
|
||||
pub duration: Option<GenerateSeedance10LiteDuration>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateSeedance10LiteResolution {
|
||||
FourEightyP,
|
||||
SevenTwentyP,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateSeedance10LiteDuration {
|
||||
FiveSeconds,
|
||||
TenSeconds,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct GenerateSeedance10LiteImageToVideoResponse {
|
||||
pub success: bool,
|
||||
pub inference_job_token: InferenceJobToken,
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub const GENERATE_VEO_2_IMAGE_TO_VIDEO_URL_PATH: &str = "/v1/generate/video/veo_2_image_to_video";
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct GenerateVeo2ImageToVideoRequest {
|
||||
/// Idempotency token to prevent duplicate requests.
|
||||
pub uuid_idempotency_token: String,
|
||||
|
||||
/// Source of the image file to convert to video.
|
||||
/// It must be an image.
|
||||
pub media_file_token: Option<MediaFileToken>,
|
||||
|
||||
/// Optional text prompt.
|
||||
pub prompt: Option<String>,
|
||||
|
||||
/// Optional: aspect ratio of the generated video.
|
||||
pub aspect_ratio: Option<GenerateVeo2AspectRatio>,
|
||||
|
||||
/// Optional: aspect ratio of the generated video.
|
||||
pub duration: Option<GenerateVeo2Duration>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateVeo2AspectRatio {
|
||||
Auto,
|
||||
AutoPreferPortrait,
|
||||
WideSixteenNine, // 16:9
|
||||
TallNineSixteen, // 9:16
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateVeo2Duration {
|
||||
FiveSeconds,
|
||||
SixSeconds,
|
||||
SevenSeconds,
|
||||
EightSeconds,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct GenerateVeo2ImageToVideoResponse {
|
||||
pub success: bool,
|
||||
pub inference_job_token: InferenceJobToken,
|
||||
}
|
||||
@@ -1 +1,5 @@
|
||||
pub mod generate_kling_1_6_pro_image_to_video;
|
||||
pub mod generate_kling_1_6_pro_image_to_video;
|
||||
pub mod generate_kling_2_1_master_image_to_video;
|
||||
pub mod generate_kling_2_1_pro_image_to_video;
|
||||
pub mod generate_seedance_1_0_lite_image_to_video;
|
||||
pub mod generate_veo_2_image_to_video;
|
||||
@@ -1,5 +1,47 @@
|
||||
use utoipa::OpenApi;
|
||||
|
||||
use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageAspectRatio;
|
||||
use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageNumImages;
|
||||
use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageRequest;
|
||||
use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageResponse;
|
||||
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageAspectRatio;
|
||||
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageNumImages;
|
||||
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageRequest;
|
||||
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageResponse;
|
||||
use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageAspectRatio;
|
||||
use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageNumImages;
|
||||
use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageRequest;
|
||||
use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageResponse;
|
||||
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageAspectRatio;
|
||||
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageNumImages;
|
||||
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageRequest;
|
||||
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageResponse;
|
||||
use artcraft_api_defs::generate::image::remove_image_background::RemoveImageBackgroundRequest;
|
||||
use artcraft_api_defs::generate::image::remove_image_background::RemoveImageBackgroundResponse;
|
||||
use artcraft_api_defs::generate::object::generate_hunyuan_21_image_to_3d::GenerateHunyuan21ImageTo3dRequest;
|
||||
use artcraft_api_defs::generate::object::generate_hunyuan_21_image_to_3d::GenerateHunyuan21ImageTo3dResponse;
|
||||
use artcraft_api_defs::generate::object::generate_hunyuan_2_image_to_3d::GenerateHunyuan2ImageTo3dRequest;
|
||||
use artcraft_api_defs::generate::object::generate_hunyuan_2_image_to_3d::GenerateHunyuan2ImageTo3dResponse;
|
||||
use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProAspectRatio;
|
||||
use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProDuration;
|
||||
use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProImageToVideoRequest;
|
||||
use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProImageToVideoResponse;
|
||||
use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::GenerateKling21MasterAspectRatio;
|
||||
use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::GenerateKling21MasterDuration;
|
||||
use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::GenerateKling21MasterImageToVideoRequest;
|
||||
use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::GenerateKling21MasterImageToVideoResponse;
|
||||
use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::GenerateKling21ProAspectRatio;
|
||||
use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::GenerateKling21ProDuration;
|
||||
use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::GenerateKling21ProImageToVideoRequest;
|
||||
use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::GenerateKling21ProImageToVideoResponse;
|
||||
use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteDuration;
|
||||
use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteImageToVideoRequest;
|
||||
use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteImageToVideoResponse;
|
||||
use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteResolution;
|
||||
use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2AspectRatio;
|
||||
use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2Duration;
|
||||
use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2ImageToVideoRequest;
|
||||
use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2ImageToVideoResponse;
|
||||
use billing_component::stripe::http_endpoints::checkout::create::stripe_create_checkout_session_error::CreateCheckoutSessionError;
|
||||
use billing_component::stripe::http_endpoints::checkout::create::stripe_create_checkout_session_json_handler::*;
|
||||
use billing_component::users::http_endpoints::list_active_user_subscriptions_handler::*;
|
||||
@@ -187,6 +229,18 @@ use crate::http_server::web_utils::response_success_helpers::*;
|
||||
crate::http_server::endpoints::featured_items::create_featured_item_handler::create_featured_item_handler,
|
||||
crate::http_server::endpoints::featured_items::delete_featured_item_handler::delete_featured_item_handler,
|
||||
crate::http_server::endpoints::featured_items::get_is_featured_item_handler::get_is_featured_item_handler,
|
||||
crate::http_server::endpoints::generate::image::generate_flux_1_dev_text_to_image_handler::generate_flux_1_dev_text_to_image_handler,
|
||||
crate::http_server::endpoints::generate::image::generate_flux_1_schnell_text_to_image_handler::generate_flux_1_schnell_text_to_image_handler,
|
||||
crate::http_server::endpoints::generate::image::generate_flux_pro_11_text_to_image_handler::generate_flux_pro_11_text_to_image_handler,
|
||||
crate::http_server::endpoints::generate::image::generate_flux_pro_11_ultra_text_to_image_handler::generate_flux_pro_11_ultra_text_to_image_handler,
|
||||
crate::http_server::endpoints::generate::image::remove_image_background_handler::remove_image_background_handler,
|
||||
crate::http_server::endpoints::generate::object::generate_hunyuan_21_image_to_3d_handler::generate_hunyuan_21_image_to_3d_handler,
|
||||
crate::http_server::endpoints::generate::object::generate_hunyuan_2_image_to_3d_handler::generate_hunyuan_2_image_to_3d_handler,
|
||||
crate::http_server::endpoints::generate::video::generate_kling_1_6_pro_video_handler::generate_kling_1_6_pro_video_handler,
|
||||
crate::http_server::endpoints::generate::video::generate_kling_2_1_master_video_handler::generate_kling_2_1_master_video_handler,
|
||||
crate::http_server::endpoints::generate::video::generate_kling_2_1_pro_video_handler::generate_kling_2_1_pro_video_handler,
|
||||
crate::http_server::endpoints::generate::video::generate_seedance_1_0_lite_image_to_video_handler::generate_seedance_1_0_lite_image_to_video_handler,
|
||||
crate::http_server::endpoints::generate::video::generate_veo_2_image_to_video_handler::generate_veo_2_image_to_video_handler,
|
||||
crate::http_server::endpoints::image_studio::prompt::enqueue_studio_image_generation_handler::enqueue_studio_image_generation_handler,
|
||||
crate::http_server::endpoints::image_studio::upload::upload_snapshot_media_file_handler::upload_snapshot_media_file_handler,
|
||||
crate::http_server::endpoints::inference_job::delete::dismiss_finished_session_jobs_handler::dismiss_finished_session_jobs_handler,
|
||||
@@ -340,9 +394,6 @@ use crate::http_server::web_utils::response_success_helpers::*;
|
||||
AppStateServerInfo,
|
||||
AppStateStatusAlertCategory,
|
||||
AppStateStatusAlertInfo,
|
||||
EnqueueStudioImageGenRequest,
|
||||
EnqueueImageGenRequestSuccessResponse,
|
||||
EnqueueImageGenRequestError,
|
||||
AppStateSubscriptionProductKey,
|
||||
AppStateUserInfo,
|
||||
AppStateUserLocale,
|
||||
@@ -354,8 +405,6 @@ use crate::http_server::web_utils::response_success_helpers::*;
|
||||
BatchGetMediaFilesQueryParams,
|
||||
BatchGetMediaFilesSuccessResponse,
|
||||
BatchGetUserBookmarksError,
|
||||
UploadSnapshotMediaFileForm,
|
||||
UploadSnapshotMediaFileSuccessResponse,
|
||||
BatchGetUserBookmarksQueryParams,
|
||||
BatchGetUserBookmarksResponse,
|
||||
BatchGetUserRatingError,
|
||||
@@ -429,9 +478,6 @@ use crate::http_server::web_utils::response_success_helpers::*;
|
||||
EditUsernameResponse,
|
||||
EnqueueFaceFusionCropDimensions,
|
||||
EnqueueFaceFusionWorkflowError,
|
||||
EnqueueStudioGen2Request,
|
||||
EnqueueStudioGen2Response,
|
||||
EnqueueStudioGen2Error,
|
||||
EnqueueFaceFusionWorkflowRequest,
|
||||
EnqueueFaceFusionWorkflowSuccessResponse,
|
||||
EnqueueFbxToGltfRequest,
|
||||
@@ -440,10 +486,16 @@ use crate::http_server::web_utils::response_success_helpers::*;
|
||||
EnqueueGptSovitsModelDownloadError,
|
||||
EnqueueGptSovitsModelDownloadRequest,
|
||||
EnqueueGptSovitsModelDownloadSuccessResponse,
|
||||
EnqueueImageGenRequestError,
|
||||
EnqueueImageGenRequestSuccessResponse,
|
||||
EnqueueLivePortraitCropDimensions,
|
||||
EnqueueLivePortraitWorkflowError,
|
||||
EnqueueLivePortraitWorkflowRequest,
|
||||
EnqueueLivePortraitWorkflowSuccessResponse,
|
||||
EnqueueStudioGen2Error,
|
||||
EnqueueStudioGen2Request,
|
||||
EnqueueStudioGen2Response,
|
||||
EnqueueStudioImageGenRequest,
|
||||
EnqueueTTSRequest,
|
||||
EnqueueTTSRequestError,
|
||||
EnqueueTTSRequestSuccessResponse,
|
||||
@@ -457,6 +509,46 @@ use crate::http_server::web_utils::response_success_helpers::*;
|
||||
FeaturedMediaFile,
|
||||
FeaturedModelWeightForList,
|
||||
FundamentalFrequencyMethod,
|
||||
GenerateFlux1DevTextToImageAspectRatio,
|
||||
GenerateFlux1DevTextToImageNumImages,
|
||||
GenerateFlux1DevTextToImageRequest,
|
||||
GenerateFlux1DevTextToImageResponse,
|
||||
GenerateFlux1SchnellTextToImageAspectRatio,
|
||||
GenerateFlux1SchnellTextToImageNumImages,
|
||||
GenerateFlux1SchnellTextToImageRequest,
|
||||
GenerateFlux1SchnellTextToImageResponse,
|
||||
GenerateFluxPro11TextToImageAspectRatio,
|
||||
GenerateFluxPro11TextToImageNumImages,
|
||||
GenerateFluxPro11TextToImageRequest,
|
||||
GenerateFluxPro11TextToImageResponse,
|
||||
GenerateFluxPro11UltraTextToImageAspectRatio,
|
||||
GenerateFluxPro11UltraTextToImageNumImages,
|
||||
GenerateFluxPro11UltraTextToImageRequest,
|
||||
GenerateFluxPro11UltraTextToImageResponse,
|
||||
GenerateHunyuan21ImageTo3dRequest,
|
||||
GenerateHunyuan21ImageTo3dResponse,
|
||||
GenerateHunyuan2ImageTo3dRequest,
|
||||
GenerateHunyuan2ImageTo3dResponse,
|
||||
GenerateKling16ProAspectRatio,
|
||||
GenerateKling16ProDuration,
|
||||
GenerateKling16ProImageToVideoRequest,
|
||||
GenerateKling16ProImageToVideoResponse,
|
||||
GenerateKling21MasterAspectRatio,
|
||||
GenerateKling21MasterDuration,
|
||||
GenerateKling21MasterImageToVideoRequest,
|
||||
GenerateKling21MasterImageToVideoResponse,
|
||||
GenerateKling21ProAspectRatio,
|
||||
GenerateKling21ProDuration,
|
||||
GenerateKling21ProImageToVideoRequest,
|
||||
GenerateKling21ProImageToVideoResponse,
|
||||
GenerateSeedance10LiteDuration,
|
||||
GenerateSeedance10LiteImageToVideoRequest,
|
||||
GenerateSeedance10LiteImageToVideoResponse,
|
||||
GenerateSeedance10LiteResolution,
|
||||
GenerateVeo2AspectRatio,
|
||||
GenerateVeo2Duration,
|
||||
GenerateVeo2ImageToVideoRequest,
|
||||
GenerateVeo2ImageToVideoResponse,
|
||||
GetInferenceJobStatusError,
|
||||
GetInferenceJobStatusPathInfo,
|
||||
GetInferenceJobStatusSuccessResponse,
|
||||
@@ -569,6 +661,8 @@ use crate::http_server::web_utils::response_success_helpers::*;
|
||||
RedeemBetaKeyError,
|
||||
RedeemBetaKeyRequest,
|
||||
RedeemBetaKeySuccessResponse,
|
||||
RemoveImageBackgroundRequest,
|
||||
RemoveImageBackgroundResponse,
|
||||
RenameMediaFileError,
|
||||
RenameMediaFileRequest,
|
||||
RequestDetailsResponse,
|
||||
@@ -636,6 +730,8 @@ use crate::http_server::web_utils::response_success_helpers::*;
|
||||
UploadSavedSceneMediaFileSuccessResponse,
|
||||
UploadSceneSnapshotMediaFileForm,
|
||||
UploadSceneSnapshotMediaFileSuccessResponse,
|
||||
UploadSnapshotMediaFileForm,
|
||||
UploadSnapshotMediaFileSuccessResponse,
|
||||
UploadStudioShotFileForm,
|
||||
UploadStudioShotSuccessResponse,
|
||||
UploadVideoMediaSuccessResponse,
|
||||
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
use actix_http::StatusCode;
|
||||
use actix_web::{HttpResponse, HttpResponseBuilder, ResponseError};
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CommonWebError {
|
||||
// TODO: Bad input with error fields vector.
|
||||
/// Bad input with a user-facing error message.
|
||||
BadInputWithSimpleMessage(String),
|
||||
/// Entity not found.
|
||||
NotFound,
|
||||
/// Not authorized to perform the action.
|
||||
NotAuthorized,
|
||||
/// Server error with no reason.
|
||||
ServerError,
|
||||
}
|
||||
|
||||
impl Error for CommonWebError {}
|
||||
|
||||
impl Display for CommonWebError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct SerializeSimpleErrorWithoutMessage<'a> {
|
||||
success: bool,
|
||||
error_code: u16,
|
||||
error_code_str: Option<&'a str>,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct SerializeSimpleErrorWithMessage<'a> {
|
||||
success: bool,
|
||||
error_code: u16,
|
||||
error_code_str: Option<&'a str>,
|
||||
message: &'a str,
|
||||
}
|
||||
|
||||
impl ResponseError for CommonWebError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
CommonWebError::BadInputWithSimpleMessage(_) => StatusCode::BAD_REQUEST,
|
||||
CommonWebError::NotFound => StatusCode::NOT_FOUND,
|
||||
CommonWebError::NotAuthorized => StatusCode::UNAUTHORIZED,
|
||||
CommonWebError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
match self {
|
||||
CommonWebError::BadInputWithSimpleMessage(msg) => {
|
||||
HttpResponse::BadRequest()
|
||||
.json(SerializeSimpleErrorWithMessage {
|
||||
success: false,
|
||||
error_code: self.status_code().as_u16(),
|
||||
error_code_str: self.status_code().canonical_reason(),
|
||||
message: msg,
|
||||
})
|
||||
},
|
||||
_ => {
|
||||
HttpResponseBuilder::new(self.status_code())
|
||||
.json(SerializeSimpleErrorWithoutMessage {
|
||||
success: false,
|
||||
error_code: self.status_code().as_u16(),
|
||||
error_code_str: self.status_code().canonical_reason(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use actix_http::body::MessageBody;
|
||||
|
||||
#[test]
|
||||
fn test_bad_input_json() {
|
||||
let error = CommonWebError::BadInputWithSimpleMessage("foo bar baz".to_string());
|
||||
let response = error.error_response();
|
||||
let bytes = response.into_body().try_into_bytes().unwrap();
|
||||
let body = String::from_utf8(bytes.to_vec()).unwrap();
|
||||
assert_eq!(body, "{\"success\":false,\"error_code\":400,\"error_code_str\":\"Bad Request\",\"message\":\"foo bar baz\"}");
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod common_web_error;
|
||||
pub mod media;
|
||||
pub mod media_file_origin_details;
|
||||
pub mod media_file_social_meta_lite;
|
||||
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::http_server::common_requests::media_file_token_path_info::MediaFileTokenPathInfo;
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::common_responses::media::media_links::MediaLinks;
|
||||
use crate::http_server::deprecated_endpoints::engine::create_scene_handler::CreateSceneError;
|
||||
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
|
||||
use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError;
|
||||
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
|
||||
use crate::http_server::web_utils::response_success_helpers::simple_json_success;
|
||||
use crate::state::server_state::ServerState;
|
||||
use crate::util::delete_role_disambiguation::{delete_role_disambiguation, DeleteRole};
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::Json;
|
||||
use actix_web::web::Path;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::{GenerateFlux1DevTextToImageAspectRatio, GenerateFlux1DevTextToImageNumImages, GenerateFlux1DevTextToImageRequest, GenerateFlux1DevTextToImageResponse};
|
||||
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
|
||||
use enums::common::visibility::Visibility;
|
||||
use fal_client::requests::webhook::image::enqueue_flux_1_dev_text_to_image_webhook::enqueue_flux_1_dev_text_to_image_webhook;
|
||||
use fal_client::requests::webhook::image::enqueue_flux_1_dev_text_to_image_webhook::{Flux1DevArgs, Flux1DevAspectRatio, Flux1DevNumImages};
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use http_server_common::response::serialize_as_json_error::serialize_as_json_error;
|
||||
use idempotency::uuid::generate_random_uuid;
|
||||
use log::{error, info, warn};
|
||||
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::insert_generic_inference_job_for_fal_queue;
|
||||
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory;
|
||||
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::InsertGenericInferenceForFalArgs;
|
||||
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
|
||||
use mysql_queries::queries::media_files::get::get_media_file::{get_media_file, MediaFile};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Flux 1 Dev text to image
|
||||
#[utoipa::path(
|
||||
post,
|
||||
tag = "Generate Images",
|
||||
path = "/v1/generate/image/flux_1_dev_text_to_image",
|
||||
responses(
|
||||
(status = 200, description = "Success", body = GenerateFlux1DevTextToImageResponse),
|
||||
),
|
||||
params(
|
||||
("request" = GenerateFlux1DevTextToImageRequest, description = "Payload for Request"),
|
||||
)
|
||||
)]
|
||||
pub async fn generate_flux_1_dev_text_to_image_handler(
|
||||
http_request: HttpRequest,
|
||||
request: Json<GenerateFlux1DevTextToImageRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<Json<GenerateFlux1DevTextToImageResponse>, CommonWebError> {
|
||||
let maybe_user_session = server_state
|
||||
.session_checker
|
||||
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
|
||||
let maybe_avt_token = server_state
|
||||
.avt_cookie_manager
|
||||
.get_avt_token_from_request(&http_request);
|
||||
|
||||
// TODO: Limit usage for new accounts. Billing, free credits metering, etc.
|
||||
|
||||
//let user_session = match maybe_user_session {
|
||||
// Some(session) => session,
|
||||
// None => {
|
||||
// warn!("not logged in");
|
||||
// return Err(CommonWebError::NotAuthorized);
|
||||
// }
|
||||
//};
|
||||
|
||||
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
|
||||
}
|
||||
|
||||
insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error inserting idempotency token: {:?}", err);
|
||||
CommonWebError::BadInputWithSimpleMessage("repeated idempotency token".to_string())
|
||||
})?;
|
||||
|
||||
const IS_MOD : bool = false;
|
||||
|
||||
info!("Fal webhook URL: {}", server_state.fal.webhook_url);
|
||||
|
||||
let aspect_ratio = match request.aspect_ratio {
|
||||
Some(GenerateFlux1DevTextToImageAspectRatio::Square) => Flux1DevAspectRatio::Square,
|
||||
Some(GenerateFlux1DevTextToImageAspectRatio::SquareHd) => Flux1DevAspectRatio::SquareHd,
|
||||
Some(GenerateFlux1DevTextToImageAspectRatio::LandscapeFourByThree) => Flux1DevAspectRatio::LandscapeFourByThree,
|
||||
Some(GenerateFlux1DevTextToImageAspectRatio::LandscapeSixteenByNine) => Flux1DevAspectRatio::LandscapeSixteenByNine,
|
||||
Some(GenerateFlux1DevTextToImageAspectRatio::PortraitThreeByFour) => Flux1DevAspectRatio::PortraitThreeByFour,
|
||||
Some(GenerateFlux1DevTextToImageAspectRatio::PortraitNineBySixteen) => Flux1DevAspectRatio::PortraitNineBySixteen,
|
||||
None => Flux1DevAspectRatio::LandscapeSixteenByNine, // Default
|
||||
};
|
||||
|
||||
let num_images = match request.num_images {
|
||||
Some(GenerateFlux1DevTextToImageNumImages::One) => Flux1DevNumImages::One,
|
||||
Some(GenerateFlux1DevTextToImageNumImages::Two) => Flux1DevNumImages::Two,
|
||||
Some(GenerateFlux1DevTextToImageNumImages::Three) => Flux1DevNumImages::Three,
|
||||
Some(GenerateFlux1DevTextToImageNumImages::Four) => Flux1DevNumImages::Four,
|
||||
None => Flux1DevNumImages::One, // Default
|
||||
};
|
||||
|
||||
let args = Flux1DevArgs {
|
||||
prompt: request.prompt.as_deref().unwrap_or(""),
|
||||
webhook_url: &server_state.fal.webhook_url,
|
||||
api_key: &server_state.fal.api_key,
|
||||
aspect_ratio,
|
||||
num_images,
|
||||
};
|
||||
|
||||
let fal_result = enqueue_flux_1_dev_text_to_image_webhook(args)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("Error calling enqueue_flux_1_dev_text_to_image_webhook: {:?}", err);
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
|
||||
let external_job_id = fal_result.request_id
|
||||
.ok_or_else(|| {
|
||||
warn!("Fal request_id is None");
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
|
||||
info!("Fal request_id: {}", external_job_id);
|
||||
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
|
||||
let db_result = insert_generic_inference_job_for_fal_queue(InsertGenericInferenceForFalArgs {
|
||||
uuid_idempotency_token: &request.uuid_idempotency_token,
|
||||
maybe_external_third_party_id: &external_job_id,
|
||||
fal_category: FalCategory::ImageGeneration,
|
||||
maybe_inference_args: None,
|
||||
maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token),
|
||||
maybe_avt_token: maybe_avt_token.as_ref(),
|
||||
creator_ip_address: &ip_address,
|
||||
creator_set_visibility: Visibility::Public,
|
||||
mysql_pool: &server_state.mysql_pool,
|
||||
}).await;
|
||||
|
||||
let job_token = match db_result {
|
||||
Ok(token) => token,
|
||||
Err(err) => {
|
||||
warn!("Error inserting generic inference job for FAL queue: {:?}", err);
|
||||
return Err(CommonWebError::ServerError);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Json(GenerateFlux1DevTextToImageResponse {
|
||||
success: true,
|
||||
inference_job_token: job_token,
|
||||
}))
|
||||
}
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::http_server::common_requests::media_file_token_path_info::MediaFileTokenPathInfo;
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::common_responses::media::media_links::MediaLinks;
|
||||
use crate::http_server::deprecated_endpoints::engine::create_scene_handler::CreateSceneError;
|
||||
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
|
||||
use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError;
|
||||
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
|
||||
use crate::http_server::web_utils::response_success_helpers::simple_json_success;
|
||||
use crate::state::server_state::ServerState;
|
||||
use crate::util::delete_role_disambiguation::{delete_role_disambiguation, DeleteRole};
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::Json;
|
||||
use actix_web::web::Path;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::{GenerateFlux1SchnellTextToImageAspectRatio, GenerateFlux1SchnellTextToImageNumImages, GenerateFlux1SchnellTextToImageRequest, GenerateFlux1SchnellTextToImageResponse};
|
||||
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
|
||||
use enums::common::visibility::Visibility;
|
||||
use fal_client::requests::webhook::image::enqueue_flux_1_schnell_text_to_image_webhook::enqueue_flux_1_schnell_text_to_image_webhook;
|
||||
use fal_client::requests::webhook::image::enqueue_flux_1_schnell_text_to_image_webhook::{Flux1SchnellArgs, Flux1SchnellAspectRatio, Flux1SchnellNumImages};
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use http_server_common::response::serialize_as_json_error::serialize_as_json_error;
|
||||
use idempotency::uuid::generate_random_uuid;
|
||||
use log::{error, info, warn};
|
||||
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::insert_generic_inference_job_for_fal_queue;
|
||||
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory;
|
||||
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::InsertGenericInferenceForFalArgs;
|
||||
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
|
||||
use mysql_queries::queries::media_files::get::get_media_file::{get_media_file, MediaFile};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Flux 1 Schnell text to image
|
||||
#[utoipa::path(
|
||||
post,
|
||||
tag = "Generate Images",
|
||||
path = "/v1/generate/image/flux_1_schnell_text_to_image",
|
||||
responses(
|
||||
(status = 200, description = "Success", body = GenerateFlux1SchnellTextToImageResponse),
|
||||
),
|
||||
params(
|
||||
("request" = GenerateFlux1SchnellTextToImageRequest, description = "Payload for Request"),
|
||||
)
|
||||
)]
|
||||
pub async fn generate_flux_1_schnell_text_to_image_handler(
|
||||
http_request: HttpRequest,
|
||||
request: Json<GenerateFlux1SchnellTextToImageRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<Json<GenerateFlux1SchnellTextToImageResponse>, CommonWebError> {
|
||||
let maybe_user_session = server_state
|
||||
.session_checker
|
||||
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
|
||||
let maybe_avt_token = server_state
|
||||
.avt_cookie_manager
|
||||
.get_avt_token_from_request(&http_request);
|
||||
|
||||
// TODO: Limit usage for new accounts. Billing, free credits metering, etc.
|
||||
|
||||
//let user_session = match maybe_user_session {
|
||||
// Some(session) => session,
|
||||
// None => {
|
||||
// warn!("not logged in");
|
||||
// return Err(CommonWebError::NotAuthorized);
|
||||
// }
|
||||
//};
|
||||
|
||||
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
|
||||
}
|
||||
|
||||
insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error inserting idempotency token: {:?}", err);
|
||||
CommonWebError::BadInputWithSimpleMessage("repeated idempotency token".to_string())
|
||||
})?;
|
||||
|
||||
const IS_MOD : bool = false;
|
||||
|
||||
info!("Fal webhook URL: {}", server_state.fal.webhook_url);
|
||||
|
||||
let aspect_ratio = match request.aspect_ratio {
|
||||
Some(GenerateFlux1SchnellTextToImageAspectRatio::Square) => Flux1SchnellAspectRatio::Square,
|
||||
Some(GenerateFlux1SchnellTextToImageAspectRatio::SquareHd) => Flux1SchnellAspectRatio::SquareHd,
|
||||
Some(GenerateFlux1SchnellTextToImageAspectRatio::LandscapeFourByThree) => Flux1SchnellAspectRatio::LandscapeFourByThree,
|
||||
Some(GenerateFlux1SchnellTextToImageAspectRatio::LandscapeSixteenByNine) => Flux1SchnellAspectRatio::LandscapeSixteenByNine,
|
||||
Some(GenerateFlux1SchnellTextToImageAspectRatio::PortraitThreeByFour) => Flux1SchnellAspectRatio::PortraitThreeByFour,
|
||||
Some(GenerateFlux1SchnellTextToImageAspectRatio::PortraitNineBySixteen) => Flux1SchnellAspectRatio::PortraitNineBySixteen,
|
||||
None => Flux1SchnellAspectRatio::LandscapeSixteenByNine, // Default
|
||||
};
|
||||
|
||||
let num_images = match request.num_images {
|
||||
Some(GenerateFlux1SchnellTextToImageNumImages::One) => Flux1SchnellNumImages::One,
|
||||
Some(GenerateFlux1SchnellTextToImageNumImages::Two) => Flux1SchnellNumImages::Two,
|
||||
Some(GenerateFlux1SchnellTextToImageNumImages::Three) => Flux1SchnellNumImages::Three,
|
||||
Some(GenerateFlux1SchnellTextToImageNumImages::Four) => Flux1SchnellNumImages::Four,
|
||||
None => Flux1SchnellNumImages::One, // Default
|
||||
};
|
||||
|
||||
let args = Flux1SchnellArgs {
|
||||
prompt: request.prompt.as_deref().unwrap_or(""),
|
||||
webhook_url: &server_state.fal.webhook_url,
|
||||
api_key: &server_state.fal.api_key,
|
||||
aspect_ratio,
|
||||
num_images,
|
||||
};
|
||||
|
||||
let fal_result = enqueue_flux_1_schnell_text_to_image_webhook(args)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("Error calling enqueue_flux_1_schnell_text_to_image_webhook: {:?}", err);
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
|
||||
let external_job_id = fal_result.request_id
|
||||
.ok_or_else(|| {
|
||||
warn!("Fal request_id is None");
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
|
||||
info!("Fal request_id: {}", external_job_id);
|
||||
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
|
||||
let db_result = insert_generic_inference_job_for_fal_queue(InsertGenericInferenceForFalArgs {
|
||||
uuid_idempotency_token: &request.uuid_idempotency_token,
|
||||
maybe_external_third_party_id: &external_job_id,
|
||||
fal_category: FalCategory::ImageGeneration,
|
||||
maybe_inference_args: None,
|
||||
maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token),
|
||||
maybe_avt_token: maybe_avt_token.as_ref(),
|
||||
creator_ip_address: &ip_address,
|
||||
creator_set_visibility: Visibility::Public,
|
||||
mysql_pool: &server_state.mysql_pool,
|
||||
}).await;
|
||||
|
||||
let job_token = match db_result {
|
||||
Ok(token) => token,
|
||||
Err(err) => {
|
||||
warn!("Error inserting generic inference job for FAL queue: {:?}", err);
|
||||
return Err(CommonWebError::ServerError);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Json(GenerateFlux1SchnellTextToImageResponse {
|
||||
success: true,
|
||||
inference_job_token: job_token,
|
||||
}))
|
||||
}
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::http_server::common_requests::media_file_token_path_info::MediaFileTokenPathInfo;
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::common_responses::media::media_links::MediaLinks;
|
||||
use crate::http_server::deprecated_endpoints::engine::create_scene_handler::CreateSceneError;
|
||||
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
|
||||
use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError;
|
||||
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
|
||||
use crate::http_server::web_utils::response_success_helpers::simple_json_success;
|
||||
use crate::state::server_state::ServerState;
|
||||
use crate::util::delete_role_disambiguation::{delete_role_disambiguation, DeleteRole};
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::Json;
|
||||
use actix_web::web::Path;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageResponse;
|
||||
use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::{GenerateFluxPro11TextToImageAspectRatio, GenerateFluxPro11TextToImageNumImages, GenerateFluxPro11TextToImageRequest};
|
||||
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
|
||||
use enums::common::visibility::Visibility;
|
||||
use fal_client::requests::webhook::image::enqueue_flux_pro_11_text_to_image_webhook::FluxPro11Args;
|
||||
use fal_client::requests::webhook::image::enqueue_flux_pro_11_text_to_image_webhook::{enqueue_flux_pro_11_text_to_image_webhook, FluxPro11AspectRatio, FluxPro11NumImages};
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use http_server_common::response::serialize_as_json_error::serialize_as_json_error;
|
||||
use idempotency::uuid::generate_random_uuid;
|
||||
use log::{error, info, warn};
|
||||
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::insert_generic_inference_job_for_fal_queue;
|
||||
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory;
|
||||
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::InsertGenericInferenceForFalArgs;
|
||||
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
|
||||
use mysql_queries::queries::media_files::get::get_media_file::{get_media_file, MediaFile};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
|
||||
/// Flux Pro 1.1
|
||||
#[utoipa::path(
|
||||
post,
|
||||
tag = "Generate Images",
|
||||
path = "/v1/generate/image/flux_pro_1.1_text_to_image",
|
||||
responses(
|
||||
(status = 200, description = "Success", body = GenerateFluxPro11TextToImageResponse),
|
||||
),
|
||||
params(
|
||||
("request" = GenerateFluxPro11TextToImageRequest, description = "Payload for Request"),
|
||||
)
|
||||
)]
|
||||
pub async fn generate_flux_pro_11_text_to_image_handler(
|
||||
http_request: HttpRequest,
|
||||
request: Json<GenerateFluxPro11TextToImageRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<Json<GenerateFluxPro11TextToImageResponse>, CommonWebError> {
|
||||
let maybe_user_session = server_state
|
||||
.session_checker
|
||||
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
|
||||
let maybe_avt_token = server_state
|
||||
.avt_cookie_manager
|
||||
.get_avt_token_from_request(&http_request);
|
||||
|
||||
// TODO: Limit usage for new accounts. Billing, free credits metering, etc.
|
||||
|
||||
//let user_session = match maybe_user_session {
|
||||
// Some(session) => session,
|
||||
// None => {
|
||||
// warn!("not logged in");
|
||||
// return Err(GenerateFluxProUltraTextToImageError::NotAuthorized);
|
||||
// }
|
||||
//};
|
||||
|
||||
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
|
||||
}
|
||||
|
||||
insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error inserting idempotency token: {:?}", err);
|
||||
CommonWebError::BadInputWithSimpleMessage("invalid idempotency token".to_string())
|
||||
})?;
|
||||
|
||||
const IS_MOD : bool = false;
|
||||
|
||||
info!("Fal webhook URL: {}", server_state.fal.webhook_url);
|
||||
|
||||
let aspect_ratio = match request.aspect_ratio {
|
||||
Some(GenerateFluxPro11TextToImageAspectRatio::Square) => FluxPro11AspectRatio::Square,
|
||||
Some(GenerateFluxPro11TextToImageAspectRatio::LandscapeFourByThree) => FluxPro11AspectRatio::LandscapeFourByThree,
|
||||
Some(GenerateFluxPro11TextToImageAspectRatio::LandscapeSixteenByNine) => FluxPro11AspectRatio::LandscapeSixteenByNine,
|
||||
Some(GenerateFluxPro11TextToImageAspectRatio::PortraitThreeByFour) => FluxPro11AspectRatio::PortraitThreeByFour,
|
||||
Some(GenerateFluxPro11TextToImageAspectRatio::PortraitNineBySixteen) => FluxPro11AspectRatio::PortraitNineBySixteen,
|
||||
None => FluxPro11AspectRatio::LandscapeSixteenByNine, // Default
|
||||
};
|
||||
|
||||
let num_images = match request.num_images {
|
||||
Some(GenerateFluxPro11TextToImageNumImages::One) => FluxPro11NumImages::One,
|
||||
Some(GenerateFluxPro11TextToImageNumImages::Two) => FluxPro11NumImages::Two,
|
||||
Some(GenerateFluxPro11TextToImageNumImages::Three) => FluxPro11NumImages::Three,
|
||||
Some(GenerateFluxPro11TextToImageNumImages::Four) => FluxPro11NumImages::Four,
|
||||
None => FluxPro11NumImages::One, // Default
|
||||
};
|
||||
|
||||
let args = FluxPro11Args {
|
||||
prompt: request.prompt.as_deref().unwrap_or(""),
|
||||
webhook_url: &server_state.fal.webhook_url,
|
||||
api_key: &server_state.fal.api_key,
|
||||
aspect_ratio,
|
||||
num_images,
|
||||
};
|
||||
|
||||
let fal_result = enqueue_flux_pro_11_text_to_image_webhook(args)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("Error calling enqueue_flux_pro_text_to_image_webhook: {:?}", err);
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
|
||||
let external_job_id = fal_result.request_id
|
||||
.ok_or_else(|| {
|
||||
warn!("Fal request_id is None");
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
|
||||
info!("Fal request_id: {}", external_job_id);
|
||||
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
|
||||
let db_result = insert_generic_inference_job_for_fal_queue(InsertGenericInferenceForFalArgs {
|
||||
uuid_idempotency_token: &request.uuid_idempotency_token,
|
||||
maybe_external_third_party_id: &external_job_id,
|
||||
fal_category: FalCategory::ImageGeneration,
|
||||
maybe_inference_args: None,
|
||||
maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token),
|
||||
maybe_avt_token: maybe_avt_token.as_ref(),
|
||||
creator_ip_address: &ip_address,
|
||||
creator_set_visibility: Visibility::Public,
|
||||
mysql_pool: &server_state.mysql_pool,
|
||||
}).await;
|
||||
|
||||
let job_token = match db_result {
|
||||
Ok(token) => token,
|
||||
Err(err) => {
|
||||
warn!("Error inserting generic inference job for FAL queue: {:?}", err);
|
||||
return Err(CommonWebError::ServerError);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Json(GenerateFluxPro11TextToImageResponse {
|
||||
success: true,
|
||||
inference_job_token: job_token,
|
||||
}))
|
||||
}
|
||||
+33
-49
@@ -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<GenerateFluxPro11UltraTextToImageRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<Json<GenerateFluxPro11UltraTextToImageResponse>, GenerateFluxProUltraTextToImageError> {
|
||||
) -> Result<Json<GenerateFluxPro11UltraTextToImageResponse>, CommonWebError> {
|
||||
let maybe_user_session = server_state
|
||||
.session_checker
|
||||
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
+14
-50
@@ -2,9 +2,12 @@ use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::http_server::common_requests::media_file_token_path_info::MediaFileTokenPathInfo;
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::common_responses::media::media_links::MediaLinks;
|
||||
use crate::http_server::deprecated_endpoints::engine::create_scene_handler::CreateSceneError;
|
||||
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
|
||||
use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError;
|
||||
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
|
||||
use crate::http_server::web_utils::response_success_helpers::simple_json_success;
|
||||
use crate::state::server_state::ServerState;
|
||||
use crate::util::delete_role_disambiguation::{delete_role_disambiguation, DeleteRole};
|
||||
@@ -29,42 +32,6 @@ use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_
|
||||
use mysql_queries::queries::media_files::get::get_media_file::{get_media_file, MediaFile};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use utoipa::ToSchema;
|
||||
use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError;
|
||||
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
|
||||
// =============== Error Response ===============
|
||||
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub enum RemoveImageBackgroundError {
|
||||
BadInput(String),
|
||||
NotFound,
|
||||
NotAuthorized,
|
||||
ServerError,
|
||||
}
|
||||
|
||||
impl ResponseError for RemoveImageBackgroundError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
RemoveImageBackgroundError::BadInput(_) => StatusCode::BAD_REQUEST,
|
||||
RemoveImageBackgroundError::NotFound => StatusCode::NOT_FOUND,
|
||||
RemoveImageBackgroundError::NotAuthorized => StatusCode::UNAUTHORIZED,
|
||||
RemoveImageBackgroundError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
serialize_as_json_error(self)
|
||||
}
|
||||
}
|
||||
|
||||
// NB: Not using derive_more::Display since Clion doesn't understand it.
|
||||
impl fmt::Display for RemoveImageBackgroundError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
// =============== Handler ===============
|
||||
|
||||
/// Background removal
|
||||
#[utoipa::path(
|
||||
@@ -73,9 +40,6 @@ impl fmt::Display for RemoveImageBackgroundError {
|
||||
path = "/v1/generate/image/remove_background",
|
||||
responses(
|
||||
(status = 200, description = "Success", body = RemoveImageBackgroundResponse),
|
||||
(status = 400, description = "Bad input", body = RemoveImageBackgroundError),
|
||||
(status = 401, description = "Not authorized", body = RemoveImageBackgroundError),
|
||||
(status = 500, description = "Server error", body = RemoveImageBackgroundError),
|
||||
),
|
||||
params(
|
||||
("request" = RemoveImageBackgroundRequest, description = "Payload for Request"),
|
||||
@@ -85,14 +49,14 @@ pub async fn remove_image_background_handler(
|
||||
http_request: HttpRequest,
|
||||
request: Json<RemoveImageBackgroundRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<Json<RemoveImageBackgroundResponse>, RemoveImageBackgroundError> {
|
||||
) -> Result<Json<RemoveImageBackgroundResponse>, CommonWebError> {
|
||||
let maybe_user_session = server_state
|
||||
.session_checker
|
||||
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
RemoveImageBackgroundError::ServerError
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
|
||||
let maybe_avt_token = server_state
|
||||
@@ -113,19 +77,19 @@ pub async fn remove_image_background_handler(
|
||||
Some(token) => token,
|
||||
None => {
|
||||
warn!("No media file token provided");
|
||||
return Err(RemoveImageBackgroundError::BadInput("No media file token provided".to_string()));
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage("No media file token provided".to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
|
||||
return Err(RemoveImageBackgroundError::BadInput(reason));
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
|
||||
}
|
||||
|
||||
insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error inserting idempotency token: {:?}", err);
|
||||
RemoveImageBackgroundError::BadInput("invalid idempotency token".to_string())
|
||||
CommonWebError::BadInputWithSimpleMessage("invalid idempotency token".to_string())
|
||||
})?;
|
||||
const IS_MOD : bool = false;
|
||||
|
||||
@@ -139,16 +103,16 @@ pub async fn remove_image_background_handler(
|
||||
Ok(Some(media_file)) => media_file,
|
||||
Ok(None) => {
|
||||
warn!("MediaFile not found: {:?}", media_file_token);
|
||||
return Err(RemoveImageBackgroundError::NotFound);
|
||||
return Err(CommonWebError::NotFound);
|
||||
},
|
||||
Err(err) => {
|
||||
warn!("Error looking up media_file: {:?}", err);
|
||||
return Err(RemoveImageBackgroundError::ServerError);
|
||||
return Err(CommonWebError::ServerError);
|
||||
}
|
||||
};
|
||||
|
||||
if !media_file.media_type.is_jpg_or_png_or_legacy_image() {
|
||||
return Err(RemoveImageBackgroundError::BadInput("Media file must be a JPG or PNG image".to_string()));
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage("Media file must be a JPG or PNG image".to_string()));
|
||||
}
|
||||
|
||||
let media_domain = get_media_domain(&http_request);
|
||||
@@ -175,13 +139,13 @@ pub async fn remove_image_background_handler(
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("Error calling remove_background_rembg_webhook: {:?}", err);
|
||||
RemoveImageBackgroundError::ServerError
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
|
||||
let external_job_id = fal_result.request_id
|
||||
.ok_or_else(|| {
|
||||
warn!("Fal request_id is None");
|
||||
RemoveImageBackgroundError::ServerError
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
|
||||
info!("Fal request_id: {}", external_job_id);
|
||||
@@ -204,7 +168,7 @@ pub async fn remove_image_background_handler(
|
||||
Ok(token) => token,
|
||||
Err(err) => {
|
||||
warn!("Error inserting generic inference job for FAL queue: {:?}", err);
|
||||
return Err(RemoveImageBackgroundError::ServerError);
|
||||
return Err(CommonWebError::ServerError);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::http_server::common_requests::media_file_token_path_info::MediaFileTokenPathInfo;
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::common_responses::media::media_links::MediaLinks;
|
||||
use crate::http_server::deprecated_endpoints::engine::create_scene_handler::CreateSceneError;
|
||||
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
|
||||
use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError;
|
||||
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
|
||||
use crate::http_server::web_utils::response_success_helpers::simple_json_success;
|
||||
use crate::state::server_state::ServerState;
|
||||
use crate::util::delete_role_disambiguation::{delete_role_disambiguation, DeleteRole};
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::Json;
|
||||
use actix_web::web::Path;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use artcraft_api_defs::generate::object::generate_hunyuan_21_image_to_3d::GenerateHunyuan21ImageTo3dRequest;
|
||||
use artcraft_api_defs::generate::object::generate_hunyuan_21_image_to_3d::GenerateHunyuan21ImageTo3dResponse;
|
||||
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
|
||||
use enums::common::visibility::Visibility;
|
||||
use fal_client::requests::webhook::object::enqueue_hunyuan_3d_21_image_to_3d_webhook::enqueue_hunyuan_3d_2_1_image_to_3d_webhook;
|
||||
use fal_client::requests::webhook::object::enqueue_hunyuan_3d_21_image_to_3d_webhook::Hunyuan3d21Args;
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use http_server_common::response::serialize_as_json_error::serialize_as_json_error;
|
||||
use idempotency::uuid::generate_random_uuid;
|
||||
use log::{error, info, warn};
|
||||
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::insert_generic_inference_job_for_fal_queue;
|
||||
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory;
|
||||
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::InsertGenericInferenceForFalArgs;
|
||||
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
|
||||
use mysql_queries::queries::media_files::get::get_media_file::{get_media_file, MediaFile};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Hunyuan 2.1 Image to 3D
|
||||
#[utoipa::path(
|
||||
post,
|
||||
tag = "Generate Objects",
|
||||
path = "/v1/generate/object/hunyuan_2.1_image_to_3d",
|
||||
responses(
|
||||
(status = 200, description = "Success", body = GenerateHunyuan21ImageTo3dResponse),
|
||||
),
|
||||
params(
|
||||
("request" = GenerateHunyuan21ImageTo3dRequest, description = "Payload for Request"),
|
||||
)
|
||||
)]
|
||||
pub async fn generate_hunyuan_21_image_to_3d_handler(
|
||||
http_request: HttpRequest,
|
||||
request: Json<GenerateHunyuan21ImageTo3dRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<Json<GenerateHunyuan21ImageTo3dResponse>, CommonWebError> {
|
||||
let maybe_user_session = server_state
|
||||
.session_checker
|
||||
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
|
||||
let maybe_avt_token = server_state
|
||||
.avt_cookie_manager
|
||||
.get_avt_token_from_request(&http_request);
|
||||
|
||||
// TODO: Limit usage for new accounts. Billing, free credits metering, etc.
|
||||
|
||||
//let user_session = match maybe_user_session {
|
||||
// Some(session) => session,
|
||||
// None => {
|
||||
// warn!("not logged in");
|
||||
// return Err(CommonWebError::NotAuthorized);
|
||||
// }
|
||||
//};
|
||||
|
||||
let media_file_token = match &request.media_file_token {
|
||||
Some(token) => token,
|
||||
None => {
|
||||
warn!("No media file token provided");
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage("No media file token provided".to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
|
||||
}
|
||||
|
||||
insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error inserting idempotency token: {:?}", err);
|
||||
CommonWebError::BadInputWithSimpleMessage("invalid idempotency token".to_string())
|
||||
})?;
|
||||
const IS_MOD : bool = false;
|
||||
|
||||
let media_file_lookup_result = get_media_file(
|
||||
media_file_token,
|
||||
IS_MOD,
|
||||
&server_state.mysql_pool,
|
||||
).await;
|
||||
|
||||
let media_file = match media_file_lookup_result {
|
||||
Ok(Some(media_file)) => media_file,
|
||||
Ok(None) => {
|
||||
warn!("MediaFile not found: {:?}", media_file_token);
|
||||
return Err(CommonWebError::NotFound);
|
||||
},
|
||||
Err(err) => {
|
||||
warn!("Error looking up media_file: {:?}", err);
|
||||
return Err(CommonWebError::ServerError);
|
||||
}
|
||||
};
|
||||
|
||||
if !media_file.media_type.is_jpg_or_png_or_legacy_image() {
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage("Media file must be a JPG or PNG image".to_string()));
|
||||
}
|
||||
|
||||
let media_domain = get_media_domain(&http_request);
|
||||
|
||||
let bucket_path = MediaFileBucketPath::from_object_hash(
|
||||
&media_file.public_bucket_directory_hash,
|
||||
media_file.maybe_public_bucket_prefix.as_deref(),
|
||||
media_file.maybe_public_bucket_extension.as_deref());
|
||||
|
||||
let media_links = MediaLinks::from_media_path_and_env(
|
||||
media_domain,
|
||||
server_state.server_environment,
|
||||
&bucket_path);
|
||||
|
||||
info!("Fal webhook URL: {}", server_state.fal.webhook_url);
|
||||
|
||||
let args = Hunyuan3d21Args {
|
||||
image_url: media_links.cdn_url,
|
||||
webhook_url: &server_state.fal.webhook_url,
|
||||
api_key: &server_state.fal.api_key,
|
||||
};
|
||||
|
||||
let fal_result = enqueue_hunyuan_3d_2_1_image_to_3d_webhook(args)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("Error calling enqueue_hunyuan_3d_2_1_image_to_3d_webhook: {:?}", err);
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
|
||||
let external_job_id = fal_result.request_id
|
||||
.ok_or_else(|| {
|
||||
warn!("Fal request_id is None");
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
|
||||
info!("Fal request_id: {}", external_job_id);
|
||||
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
|
||||
let db_result = insert_generic_inference_job_for_fal_queue(InsertGenericInferenceForFalArgs {
|
||||
uuid_idempotency_token: &request.uuid_idempotency_token,
|
||||
maybe_external_third_party_id: &external_job_id,
|
||||
fal_category: FalCategory::ObjectGeneration,
|
||||
maybe_inference_args: None,
|
||||
maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token),
|
||||
maybe_avt_token: maybe_avt_token.as_ref(),
|
||||
creator_ip_address: &ip_address,
|
||||
creator_set_visibility: Visibility::Public,
|
||||
mysql_pool: &server_state.mysql_pool,
|
||||
}).await;
|
||||
|
||||
let job_token = match db_result {
|
||||
Ok(token) => token,
|
||||
Err(err) => {
|
||||
warn!("Error inserting generic inference job for FAL queue: {:?}", err);
|
||||
return Err(CommonWebError::ServerError);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Json(GenerateHunyuan21ImageTo3dResponse {
|
||||
success: true,
|
||||
inference_job_token: job_token,
|
||||
}))
|
||||
}
|
||||
+14
-51
@@ -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<GenerateHunyuan2ImageTo3dRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<Json<GenerateHunyuan2ImageTo3dResponse>, GenerateHunyuan2ImageTo3dError> {
|
||||
) -> Result<Json<GenerateHunyuan2ImageTo3dResponse>, CommonWebError> {
|
||||
let maybe_user_session = server_state
|
||||
.session_checker
|
||||
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
GenerateHunyuan2ImageTo3dError::ServerError
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
|
||||
let maybe_avt_token = server_state
|
||||
@@ -107,7 +70,7 @@ pub async fn generate_hunyuan_2_image_to_3d_handler(
|
||||
// Some(session) => session,
|
||||
// None => {
|
||||
// warn!("not logged in");
|
||||
// return Err(GenerateHunyuan2ImageTo3dError::NotAuthorized);
|
||||
// return Err(CommonWebError::NotAuthorized);
|
||||
// }
|
||||
//};
|
||||
|
||||
@@ -115,19 +78,19 @@ pub async fn generate_hunyuan_2_image_to_3d_handler(
|
||||
Some(token) => token,
|
||||
None => {
|
||||
warn!("No media file token provided");
|
||||
return Err(GenerateHunyuan2ImageTo3dError::BadInput("No media file token provided".to_string()));
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage("No media file token provided".to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
|
||||
return Err(GenerateHunyuan2ImageTo3dError::BadInput(reason));
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
|
||||
}
|
||||
|
||||
insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error inserting idempotency token: {:?}", err);
|
||||
GenerateHunyuan2ImageTo3dError::BadInput("invalid idempotency token".to_string())
|
||||
CommonWebError::BadInputWithSimpleMessage("invalid idempotency token".to_string())
|
||||
})?;
|
||||
const IS_MOD : bool = false;
|
||||
|
||||
@@ -141,16 +104,16 @@ pub async fn generate_hunyuan_2_image_to_3d_handler(
|
||||
Ok(Some(media_file)) => media_file,
|
||||
Ok(None) => {
|
||||
warn!("MediaFile not found: {:?}", media_file_token);
|
||||
return Err(GenerateHunyuan2ImageTo3dError::NotFound);
|
||||
return Err(CommonWebError::NotFound);
|
||||
},
|
||||
Err(err) => {
|
||||
warn!("Error looking up media_file: {:?}", err);
|
||||
return Err(GenerateHunyuan2ImageTo3dError::ServerError);
|
||||
return Err(CommonWebError::ServerError);
|
||||
}
|
||||
};
|
||||
|
||||
if !media_file.media_type.is_jpg_or_png_or_legacy_image() {
|
||||
return Err(GenerateHunyuan2ImageTo3dError::BadInput("Media file must be a JPG or PNG image".to_string()));
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage("Media file must be a JPG or PNG image".to_string()));
|
||||
}
|
||||
|
||||
let media_domain = get_media_domain(&http_request);
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
pub mod generate_hunyuan2_image_to_3d_handler;
|
||||
pub mod generate_hunyuan_21_image_to_3d_handler;
|
||||
pub mod generate_hunyuan_2_image_to_3d_handler;
|
||||
+25
-57
@@ -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<GenerateKling16ProImageToVideoRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<Json<GenerateKling16ProImageToVideoResponse>, GenerateKling16VideoError> {
|
||||
) -> Result<Json<GenerateKling16ProImageToVideoResponse>, 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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+226
@@ -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<ReferenceImage>,
|
||||
|
||||
*/
|
||||
|
||||
/// 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<GenerateKling21MasterImageToVideoRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<Json<GenerateKling21MasterImageToVideoResponse>, 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,
|
||||
}))
|
||||
}
|
||||
+226
@@ -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<ReferenceImage>,
|
||||
|
||||
*/
|
||||
|
||||
/// 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<GenerateKling21ProImageToVideoRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<Json<GenerateKling21ProImageToVideoResponse>, 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,
|
||||
}))
|
||||
}
|
||||
+205
@@ -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<GenerateSeedance10LiteImageToVideoRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<Json<GenerateSeedance10LiteImageToVideoResponse>, 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,
|
||||
}))
|
||||
}
|
||||
+208
@@ -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<GenerateVeo2ImageToVideoRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<Json<GenerateVeo2ImageToVideoResponse>, 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,
|
||||
}))
|
||||
}
|
||||
@@ -1 +1,5 @@
|
||||
pub mod generate_kling_1_6_pro_video_handler;
|
||||
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;
|
||||
@@ -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()))
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,38 +1,65 @@
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { AUTH_STATUS } from "~/enums";
|
||||
import { authentication } from "~/signals";
|
||||
//import { ButtonLink } from "@storyteller/ui-button-link";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import ProfileDropdown from "./ProfileDropdown";
|
||||
import { LoginModal } from "@storyteller/ui-login-modal";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
disableHotkeyInput,
|
||||
DomLevels,
|
||||
enableHotkeyInput,
|
||||
} from "~/pages/PageEnigma/signals";
|
||||
|
||||
export const AuthButtons = () => {
|
||||
useSignals();
|
||||
const [showLoginModal, setShowLoginModal] = useState(false);
|
||||
const [isSignUp, setIsSignUp] = useState(false);
|
||||
|
||||
const { status } = authentication;
|
||||
const { status, userInfo } = authentication;
|
||||
|
||||
console.log("Current State:");
|
||||
console.log(status.value);
|
||||
if (status.value === AUTH_STATUS.LOGGED_IN) {
|
||||
console.log("SHOWING DROPDOWN");
|
||||
return <ProfileDropdown />;
|
||||
} else {
|
||||
return (
|
||||
<div className="flex items-center gap-3.5">
|
||||
<span className="text-white/20">|</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* <ButtonLink
|
||||
to="/login"
|
||||
variant="secondary"
|
||||
reloadDocument={true}
|
||||
className="h-[38px]" // TODO(bt,2025-04-19): Once we have in-page routing, get rid of this.
|
||||
<>
|
||||
<div className="flex items-center gap-3.5">
|
||||
<span className="text-white/20">|</span>
|
||||
<Button
|
||||
className="h-[38px]"
|
||||
onClick={() => {
|
||||
setIsSignUp(true);
|
||||
setShowLoginModal(true);
|
||||
}}
|
||||
>
|
||||
Login
|
||||
</ButtonLink>
|
||||
<ButtonLink
|
||||
to="/signup"
|
||||
reloadDocument={true}
|
||||
className="h-[38px]" // TODO(bt,2025-04-19): Once we have in-page routing, get rid of this.
|
||||
>
|
||||
Sign Up
|
||||
</ButtonLink> */}
|
||||
Login / Sign Up
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showLoginModal && (
|
||||
<LoginModal
|
||||
onClose={() => setShowLoginModal(false)}
|
||||
isSignUp={isSignUp}
|
||||
videoSrc2D="/resources/videos/artcraft-canvas-demo.mp4"
|
||||
videoSrc3D="/resources/videos/artcraft-3d-demo.mp4"
|
||||
onArtCraftAuthSuccess={(userInfo_value) => {
|
||||
userInfo.value = userInfo_value;
|
||||
status.value = AUTH_STATUS.LOGGED_IN;
|
||||
setShowLoginModal(false);
|
||||
}}
|
||||
onOpenChange={(isOpen: boolean) => {
|
||||
if (isOpen) {
|
||||
disableHotkeyInput(DomLevels.DIALOGUE);
|
||||
} else {
|
||||
enableHotkeyInput(DomLevels.DIALOGUE);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -185,13 +185,7 @@ export const PaintSurface = ({
|
||||
isWithinLeftPanel(stagePoint)
|
||||
) {
|
||||
const lineId = `line-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
let opacity;
|
||||
|
||||
if (activeTool === "draw") {
|
||||
opacity = store.brushOpacity
|
||||
} else {
|
||||
opacity = 1
|
||||
}
|
||||
let opacity = activeTool === "draw" ? store.brushOpacity : 1;
|
||||
|
||||
const newLineNode: LineNode = {
|
||||
id: lineId,
|
||||
@@ -200,10 +194,10 @@ export const PaintSurface = ({
|
||||
stroke: activeTool === "draw" ? brushColor : fillColor || "#ffffff",
|
||||
strokeWidth: brushSize / stage.scaleX(),
|
||||
draggable: true,
|
||||
opacity: opacity // Add opacity to the line node
|
||||
opacity: opacity
|
||||
};
|
||||
store.selectNode(null);
|
||||
store.addLineNode(newLineNode);
|
||||
store.addLineNode(newLineNode, false); // Don't save state when starting line
|
||||
setCurrentLineId(lineId);
|
||||
setIsDrawing(true);
|
||||
setLastPoint(stagePoint);
|
||||
@@ -280,7 +274,6 @@ export const PaintSurface = ({
|
||||
y: (point.y - stage.y()) / stage.scaleY(),
|
||||
};
|
||||
|
||||
// Only add point if it's within the left panel bounds
|
||||
if (isWithinLeftPanel(stagePoint)) {
|
||||
const currentLine = store.lineNodes.find(
|
||||
(line) => line.id === currentLineId,
|
||||
@@ -291,7 +284,7 @@ export const PaintSurface = ({
|
||||
stagePoint.x,
|
||||
stagePoint.y,
|
||||
];
|
||||
store.updateLineNode(currentLineId, { points: updatedPoints });
|
||||
store.updateLineNode(currentLineId, { points: updatedPoints }, false); // Don't save state while drawing
|
||||
}
|
||||
setLastPoint(stagePoint);
|
||||
} else {
|
||||
@@ -339,6 +332,10 @@ export const PaintSurface = ({
|
||||
};
|
||||
|
||||
const handleStageMouseUp = () => {
|
||||
if (isDrawing) {
|
||||
store.saveState(); // Save state only when the stroke is complete
|
||||
}
|
||||
|
||||
if (isSelecting && selectionRect) {
|
||||
// Calculate the bounds of the selection rectangle
|
||||
const left = Math.min(selectionRect.startX, selectionRect.endX);
|
||||
@@ -692,7 +689,7 @@ export const PaintSurface = ({
|
||||
scaleY: finalScaleY,
|
||||
offsetX: finalOffsetX,
|
||||
offsetY: finalOffsetY,
|
||||
});
|
||||
}, true);
|
||||
} else {
|
||||
store.updateNode(
|
||||
nodeId,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
export interface MenuPosition {
|
||||
x: number;
|
||||
@@ -33,14 +34,14 @@ export const ContextMenu: React.FC<ContextMenuProps> = ({ items, onAction, onClo
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, [onClose]);
|
||||
|
||||
return (
|
||||
return createPortal(
|
||||
<div
|
||||
ref={menuRef}
|
||||
style={{
|
||||
left: `${position.x}px`,
|
||||
top: `${position.y}px`,
|
||||
}}
|
||||
className="fixed z-50 min-w-[220px] bg-[#1A1A1A] rounded-lg shadow-xl border border-[#333333] py-1 select-none"
|
||||
className="fixed z-[9999999] min-w-[220px] bg-[#1A1A1A] rounded-lg shadow-xl border border-[#333333] py-1 select-none"
|
||||
>
|
||||
{items.map((item, index) => (
|
||||
<React.Fragment key={index}>
|
||||
@@ -59,7 +60,8 @@ export const ContextMenu: React.FC<ContextMenuProps> = ({ items, onAction, onClo
|
||||
{item.divider && <div className="h-[1px] bg-[#333333] mx-2 my-1" />}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
};
|
||||
|
||||
@@ -123,7 +125,7 @@ export const ContextMenuContainer: React.FC<ContextMenuContainerProps> = ({
|
||||
<div
|
||||
onClick={handleClick}
|
||||
onContextMenu={handleContextMenu}
|
||||
className={`w-full h-full ${className}`}
|
||||
className={`w-full h-full relative z-[99999] ${className}`}
|
||||
>
|
||||
{children}
|
||||
{showMenu && (
|
||||
|
||||
@@ -7,6 +7,15 @@ interface DeleteHotkeysProps {
|
||||
|
||||
export const useDeleteHotkeys = ({ onDelete, target = document }: DeleteHotkeysProps): void => {
|
||||
const handleKeyDown = useCallback((event: KeyboardEvent) => {
|
||||
// Check if the event target is an input field or contentEditable element
|
||||
const target = event.target as HTMLElement;
|
||||
if (target.tagName === 'INPUT' ||
|
||||
target.tagName === 'TEXTAREA' ||
|
||||
target.isContentEditable ||
|
||||
target.getAttribute('role') === 'textbox') {
|
||||
return; // Don't prevent default for input fields
|
||||
}
|
||||
|
||||
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
||||
const isDelete = event.key === 'Delete' ||
|
||||
event.key === 'Backspace' ||
|
||||
|
||||
@@ -89,9 +89,9 @@ interface SceneState {
|
||||
saveState: () => void;
|
||||
|
||||
// Add new actions for line nodes
|
||||
addLineNode: (lineNode: LineNode) => void;
|
||||
addLineNode: (lineNode: LineNode, shouldSaveState: boolean) => void;
|
||||
removeLineNode: (id: string, shouldSaveState?: boolean) => void;
|
||||
updateLineNode: (id: string, updates: Partial<LineNode>) => void;
|
||||
updateLineNode: (id: string, updates: Partial<LineNode>, shouldSaveState: boolean) => void;
|
||||
moveLineNode: (id: string, dx: number, dy: number) => void;
|
||||
|
||||
// Add a specific method for file uploads
|
||||
@@ -133,7 +133,7 @@ interface SceneState {
|
||||
sendToBack: (nodeIds: string[]) => void;
|
||||
bringForward: (nodeIds: string[]) => void;
|
||||
sendBackward: (nodeIds: string[]) => void;
|
||||
removeBackground: (nodeIds: string[], operation: (success: boolean,base64_image:string, message: string) => Promise<{ success: boolean; file?: File }>) => Promise<void>;
|
||||
removeBackground: (nodeIds: string[], operation: (success: boolean, base64_image: string, message: string) => Promise<{ success: boolean; file?: File }>) => Promise<void>;
|
||||
|
||||
// Add new lock action
|
||||
toggleLock: (nodeIds: string[]) => void;
|
||||
@@ -516,17 +516,18 @@ export const useSceneStore = create<SceneState>((set, get) => ({
|
||||
},
|
||||
|
||||
// Add new line node actions
|
||||
addLineNode: (lineNode: LineNode) => {
|
||||
addLineNode: (lineNode: LineNode, shouldSaveState: boolean = true) => {
|
||||
set((state) => {
|
||||
const nextZ = getNextZIndex(state.nodes, state.lineNodes);
|
||||
|
||||
const newLineNode = {
|
||||
...lineNode,
|
||||
zIndex: nextZ
|
||||
};
|
||||
return { lineNodes: [...state.lineNodes, newLineNode] };
|
||||
});
|
||||
get().saveState();
|
||||
if (shouldSaveState) {
|
||||
get().saveState();
|
||||
}
|
||||
},
|
||||
|
||||
removeLineNode: (id: string, shouldSaveState: boolean = true) => {
|
||||
@@ -547,7 +548,7 @@ export const useSceneStore = create<SceneState>((set, get) => ({
|
||||
}
|
||||
},
|
||||
|
||||
updateLineNode: (id: string, updates: Partial<LineNode>) => {
|
||||
updateLineNode: (id: string, updates: Partial<LineNode>, shouldSaveState: boolean = true) => {
|
||||
set((state) => {
|
||||
const newLineNodes = state.lineNodes.map(node => {
|
||||
if (node.id === id) {
|
||||
@@ -561,7 +562,9 @@ export const useSceneStore = create<SceneState>((set, get) => ({
|
||||
});
|
||||
return { lineNodes: newLineNodes };
|
||||
});
|
||||
get().saveState();
|
||||
if (shouldSaveState) {
|
||||
get().saveState();
|
||||
}
|
||||
},
|
||||
|
||||
moveLineNode: (id: string, dx: number, dy: number) => {
|
||||
@@ -593,7 +596,8 @@ export const useSceneStore = create<SceneState>((set, get) => ({
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
const aspectRatio = img.naturalWidth / img.naturalHeight;
|
||||
const finalWidth = width || Math.min(img.naturalWidth, 300);
|
||||
// Increase default size to 512px width while maintaining aspect ratio
|
||||
const finalWidth = width || Math.min(img.naturalWidth, 512);
|
||||
const finalHeight = height || finalWidth / aspectRatio;
|
||||
console.log('Image loaded with dimensions:', {
|
||||
naturalWidth: img.naturalWidth,
|
||||
@@ -1158,58 +1162,46 @@ export const useSceneStore = create<SceneState>((set, get) => ({
|
||||
});
|
||||
get().saveState();
|
||||
},
|
||||
removeBackground: async (nodeIds: string[], operation: (success: boolean, base64_image:string,message: string) => Promise<{ success: boolean; file?: File }>) => {
|
||||
|
||||
removeBackground: async (nodeIds: string[], operation: (success: boolean, base64_image: string, message: string) => Promise<{ success: boolean; file?: File }>) => {
|
||||
const hasImageNodes = nodeIds.some(id => {
|
||||
const node = get().nodes.find(n => n.id === id);
|
||||
return node ? node.type === 'image': false;
|
||||
const node = get().nodes.find(n => n.id === id);
|
||||
return node ? node.type === 'image' : false;
|
||||
});
|
||||
let callbackSuccess = false
|
||||
|
||||
if (hasImageNodes) {
|
||||
console.log("Selected nodes include image types.");
|
||||
|
||||
const firstNode = get().nodes.find(node => nodeIds.includes(node.id) && node.type === 'image');
|
||||
if (firstNode) {
|
||||
const imageFile = firstNode.imageFile; // Assuming the file is stored in the node
|
||||
if (imageFile !== undefined) {
|
||||
try {
|
||||
const base64Image = await convertFileToBase64(imageFile);
|
||||
console.log("Base64 Image:", base64Image);
|
||||
// Additional logic to handle the base64 image can be added here
|
||||
const { success, file } = await operation(true, base64Image, "Removing Background."); // Indicate success in removing background
|
||||
callbackSuccess = success;
|
||||
if (callbackSuccess) {
|
||||
// save the state of the image.
|
||||
set((state: SceneState) => ({
|
||||
nodes: state.nodes.map((node: Node) => {
|
||||
if (node.id === firstNode.id) {
|
||||
// Create an object URL for immediate preview if needed
|
||||
console.log("Updated Node!")
|
||||
const updatedImageUrl: string | undefined = file ? URL.createObjectURL(file) : node.imageUrl;
|
||||
return new Node({
|
||||
...node,
|
||||
imageFile: file,
|
||||
imageUrl: updatedImageUrl,
|
||||
});
|
||||
}
|
||||
return node;
|
||||
}),
|
||||
}));
|
||||
|
||||
const firstNode = get().nodes.find(node => nodeIds.includes(node.id) && node.type === 'image');
|
||||
if (firstNode && firstNode.imageFile) {
|
||||
try {
|
||||
const base64Image = await convertFileToBase64(firstNode.imageFile);
|
||||
const { success, file } = await operation(true, base64Image, "Removing Background.");
|
||||
|
||||
if (success && file) {
|
||||
// Create a new node instance
|
||||
const updatedNode = new Node({
|
||||
...firstNode,
|
||||
imageFile: file
|
||||
});
|
||||
|
||||
// Load the new image
|
||||
await updatedNode.setImageFromFile(file);
|
||||
|
||||
// Update the store with the fully loaded node
|
||||
set((state: SceneState) => ({
|
||||
nodes: state.nodes.map((node: Node) => {
|
||||
if (node.id === firstNode.id) {
|
||||
return updatedNode;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error converting file to base64:", error);
|
||||
}
|
||||
}
|
||||
console.log("Image File:", imageFile);
|
||||
} else {
|
||||
console.warn("No image node found or first node is not an image.");
|
||||
return node;
|
||||
}),
|
||||
}));
|
||||
|
||||
get().saveState();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating image:", error);
|
||||
}
|
||||
} else {
|
||||
console.log("No selected nodes are of image type.");
|
||||
operation(false,"","Did not select a Images"); // Indicate failure to remove background
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
// Add toggleLock action
|
||||
|
||||
@@ -12,6 +12,7 @@ import React, {
|
||||
} from "react";
|
||||
import {
|
||||
FilterMediaClasses,
|
||||
FilterMediaType,
|
||||
GalleryModalApi,
|
||||
UsersApi,
|
||||
} from "@storyteller/api";
|
||||
@@ -218,7 +219,8 @@ export const GalleryModal = React.memo(
|
||||
include_user_uploads:
|
||||
activeFilter === "uploaded" ||
|
||||
activeFilter === "all" ||
|
||||
activeFilter === "3d",
|
||||
activeFilter === "3d" ||
|
||||
activeFilter === "image",
|
||||
user_uploads_only: activeFilter === "uploaded",
|
||||
page_index: reset ? 0 : pageIndex,
|
||||
page_size: pageSize,
|
||||
@@ -227,26 +229,30 @@ export const GalleryModal = React.memo(
|
||||
|
||||
if (response.success && response.data) {
|
||||
const thumbnail_size = 250;
|
||||
const newItems = response.data.map((item: any) => ({
|
||||
id: item.token,
|
||||
label: item.maybe_title || "Image Generation",
|
||||
thumbnail:
|
||||
item.media_class === "video"
|
||||
? item.media_links.maybe_video_previews.still
|
||||
: item.media_class === "dimensional"
|
||||
? item.cover_image?.maybe_cover_image_public_bucket_url
|
||||
: item.media_links.maybe_thumbnail_template?.replace(
|
||||
"{WIDTH}",
|
||||
thumbnail_size.toString()
|
||||
),
|
||||
fullImage: item.media_links.cdn_url,
|
||||
createdAt: item.created_at,
|
||||
mediaClass:
|
||||
item.media_class ||
|
||||
(item.filter_media_classes
|
||||
? item.filter_media_classes[0]
|
||||
: "image"),
|
||||
}));
|
||||
const newItems = response.data
|
||||
.filter(
|
||||
(item: any) => item.media_type !== FilterMediaType.SCENE_JSON
|
||||
)
|
||||
.map((item: any) => ({
|
||||
id: item.token,
|
||||
label: item.maybe_title || "Image Generation",
|
||||
thumbnail:
|
||||
item.media_class === "video"
|
||||
? item.media_links.maybe_video_previews.still
|
||||
: item.media_class === "dimensional"
|
||||
? item.cover_image?.maybe_cover_image_public_bucket_url
|
||||
: item.media_links.maybe_thumbnail_template?.replace(
|
||||
"{WIDTH}",
|
||||
thumbnail_size.toString()
|
||||
),
|
||||
fullImage: item.media_links.cdn_url,
|
||||
createdAt: item.created_at,
|
||||
mediaClass:
|
||||
item.media_class ||
|
||||
(item.filter_media_classes
|
||||
? item.filter_media_classes[0]
|
||||
: "image"),
|
||||
}));
|
||||
setAllItems((prev) => (reset ? newItems : [...prev, ...newItems]));
|
||||
// Pagination logic
|
||||
const current = response.pagination?.current ?? 0;
|
||||
@@ -355,13 +361,13 @@ export const GalleryModal = React.memo(
|
||||
}}
|
||||
className={twMerge(
|
||||
"h-[620px] max-w-4xl",
|
||||
mode === "view" && "h-[640px] max-w-4xl"
|
||||
mode === "view" && "h-[640px] w-[56rem]"
|
||||
)}
|
||||
childPadding={false}
|
||||
showClose={false}
|
||||
draggable={mode === "view"}
|
||||
allowBackgroundInteraction={mode === "view" ? true : false}
|
||||
closeOnOutsideClick={false}
|
||||
closeOnOutsideClick={mode === "view" ? false : true}
|
||||
>
|
||||
{mode === "view" && (
|
||||
<Modal.DragHandle>
|
||||
@@ -526,10 +532,23 @@ export const GalleryModal = React.memo(
|
||||
{Object.entries(groupItemsByDate(allItems)).map(
|
||||
([date, dateItems]) => {
|
||||
const filteredItems = dateItems.filter((item) => {
|
||||
if (activeFilter === "3d")
|
||||
if ((item as any).mediaType === "scene_json")
|
||||
return false;
|
||||
if (activeFilter === "3d") {
|
||||
return item.mediaClass === "dimensional";
|
||||
if (activeFilter === "all")
|
||||
return item.mediaClass !== "audio";
|
||||
}
|
||||
if (activeFilter === "image") {
|
||||
return item.mediaClass === "image";
|
||||
}
|
||||
if (activeFilter === "video") {
|
||||
return item.mediaClass === "video";
|
||||
}
|
||||
if (activeFilter === "all") {
|
||||
return (
|
||||
item.mediaClass !== "audio" &&
|
||||
(item as any).mediaType !== "scene_json"
|
||||
);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (filteredItems.length === 0) return null;
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { CloseButton } from "@storyteller/ui-close-button";
|
||||
import { Modal } from "@storyteller/ui-modal";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import { Transition, TransitionChild } from "@headlessui/react";
|
||||
import { createPortal } from "react-dom";
|
||||
import dayjs from "dayjs";
|
||||
import { faDownToLine } from "@fortawesome/pro-solid-svg-icons";
|
||||
import {
|
||||
FalHunyuanImageTo3d,
|
||||
EnqueueImageTo3dObject,
|
||||
EnqueueImageTo3dObjectModel,
|
||||
} from "@storyteller/tauri-api";
|
||||
import { LoadingSpinner } from "@storyteller/ui-loading-spinner";
|
||||
import React from "react";
|
||||
|
||||
interface LightboxModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -46,156 +45,145 @@ export function LightboxModal({
|
||||
onAddToSceneClicked,
|
||||
mediaClass,
|
||||
}: LightboxModalProps) {
|
||||
// NB(bt,2025-06-14): We add ?cors=1 to the image url to prevent caching "sec-fetch-mode: no-cors" from
|
||||
// NB(bt,2025-06-14): We add ?cors=1 to the image url to prevent caching "sec-fetch-mode: no-cors" from
|
||||
// the <image> tag request from being cached. If we then drag it into the canvas after it's been cached,
|
||||
// it won't be able to load in cors mode and will show blank in the canvas and 3D engine. This is a really
|
||||
// stupid hack around this behavior.
|
||||
const imageTagImageUrl = imageUrl ? imageUrl + "?cors=1" : "";
|
||||
return createPortal(
|
||||
<Transition appear show={isOpen}>
|
||||
<div className="fixed inset-0 z-[100]">
|
||||
{/* backdrop */}
|
||||
<TransitionChild
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div
|
||||
className="fixed inset-0 cursor-pointer bg-black/60"
|
||||
onClick={onClose}
|
||||
/>
|
||||
</TransitionChild>
|
||||
|
||||
{/* dialog container */}
|
||||
<div
|
||||
className="fixed inset-0 flex items-center justify-center p-4"
|
||||
onClick={onClose}
|
||||
>
|
||||
<TransitionChild
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<div
|
||||
className="relative h-[90vh] w-[80vw] rounded-xl bg-[#2C2C2C]"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* close */}
|
||||
<CloseButton
|
||||
onClick={onClose}
|
||||
className="absolute right-4 top-4 z-10"
|
||||
/>
|
||||
const [mediaLoaded, setMediaLoaded] = React.useState<boolean>(false);
|
||||
|
||||
{/* content grid */}
|
||||
<div className="grid h-full grid-cols-3 gap-6">
|
||||
{/* image panel */}
|
||||
<div className="col-span-2 flex h-full items-center justify-center overflow-hidden rounded-l-xl bg-black/40">
|
||||
{!imageUrl ? (
|
||||
<div className="flex h-full w-full items-center justify-center bg-gray-800">
|
||||
<span className="text-white/60">Image not available</span>
|
||||
</div>
|
||||
) : mediaClass === "video" ? (
|
||||
<video controls className="h-full w-full object-contain">
|
||||
<source src={imageUrl} type="video/mp4" />
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
) : (
|
||||
<img
|
||||
src={imageTagImageUrl}
|
||||
alt={imageAlt}
|
||||
className="h-full w-full object-contain"
|
||||
onError={onImageError}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
// Reset when imageUrl changes
|
||||
React.useEffect(() => {
|
||||
setMediaLoaded(false);
|
||||
}, [imageUrl]);
|
||||
|
||||
{/* info + actions */}
|
||||
<div className="flex h-full flex-col py-5 pe-5">
|
||||
<div className="flex-1 space-y-4">
|
||||
<div className="text-xl font-medium">
|
||||
{title || "Image Generation"}
|
||||
</div>
|
||||
{createdAt && (
|
||||
<div className="text-sm text-white/60">
|
||||
Created:{" "}
|
||||
{dayjs(createdAt).format("MMM D, YYYY HH:mm:ss")}
|
||||
</div>
|
||||
)}
|
||||
{additionalInfo}
|
||||
</div>
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
className="rounded-xl bg-[#2C2C2C] h-[80vh] w-[60vw] max-w-screen"
|
||||
draggable
|
||||
allowBackgroundInteraction={true}
|
||||
showClose={true}
|
||||
closeOnOutsideClick={false}
|
||||
resizable={true}
|
||||
backdropClassName="pointer-events-none hidden"
|
||||
>
|
||||
{/* Invisible drag handle strip at the very top for moving */}
|
||||
<Modal.DragHandle>
|
||||
<div className="absolute left-0 top-0 z-20 h-12 w-full cursor-move rounded-t-xl" />
|
||||
</Modal.DragHandle>
|
||||
|
||||
{/* buttons with spacing */}
|
||||
{(onAddToSceneClicked && downloadUrl) || downloadUrl ? (
|
||||
<div className="mt-15 mb-15 flex justify-end gap-2">
|
||||
<Button
|
||||
onClick={async (e) => {
|
||||
//let _result = await FalHunyuanImageTo3d({
|
||||
// image_media_token: mediaId,
|
||||
// //base64_image: downloadUrl,
|
||||
//});
|
||||
let _result = await EnqueueImageTo3dObject({
|
||||
image_media_token: mediaId,
|
||||
model: EnqueueImageTo3dObjectModel.Hunyuan3d2,
|
||||
});
|
||||
//e.stopPropagation();
|
||||
//await onAddToSceneClicked(downloadUrl, mediaId);
|
||||
//onClose(); // close the lightbox
|
||||
//onCloseGallery(); // close the gallery
|
||||
}}
|
||||
>
|
||||
3D
|
||||
</Button>
|
||||
|
||||
{onAddToSceneClicked && downloadUrl && (
|
||||
<Button
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
await onAddToSceneClicked(downloadUrl, mediaId);
|
||||
onClose(); // close the lightbox
|
||||
onCloseGallery(); // close the gallery
|
||||
}}
|
||||
>
|
||||
Add to Current Scene
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{downloadUrl &&
|
||||
(onDownloadClicked ? (
|
||||
<Button
|
||||
icon={faDownToLine}
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
await onDownloadClicked(downloadUrl);
|
||||
}}
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
) : (
|
||||
<a
|
||||
href={downloadUrl}
|
||||
download
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="no-underline"
|
||||
>
|
||||
<Button icon={faDownToLine}>Download</Button>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{/* content grid */}
|
||||
<div className="grid h-full grid-cols-3 gap-6">
|
||||
{/* image panel */}
|
||||
<div className="col-span-2 relative flex h-full items-center justify-center overflow-hidden rounded-l-xl bg-[#1A1A1A]">
|
||||
{!imageUrl ? (
|
||||
<div className="flex h-full w-full items-center justify-center bg-gray-800">
|
||||
<span className="text-white/60">Image not available</span>
|
||||
</div>
|
||||
</TransitionChild>
|
||||
) : mediaClass === "video" ? (
|
||||
<video
|
||||
controls
|
||||
className="h-full w-full object-contain"
|
||||
onLoadedData={() => setMediaLoaded(true)}
|
||||
>
|
||||
<source src={imageUrl} type="video/mp4" />
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
) : (
|
||||
<img
|
||||
src={imageTagImageUrl}
|
||||
alt={imageAlt}
|
||||
className="h-full w-full object-contain"
|
||||
onError={onImageError}
|
||||
onLoad={() => setMediaLoaded(true)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!mediaLoaded && imageUrl && (
|
||||
<div className="absolute inset-0 bg-[#1A1A1A] flex items-center justify-center">
|
||||
<LoadingSpinner className="h-12 w-12 text-white" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* info + actions */}
|
||||
<div className="flex h-full flex-col py-5 pe-5">
|
||||
<div className="flex-1 space-y-4">
|
||||
<div className="text-xl font-medium">
|
||||
{title || "Image Generation"}
|
||||
</div>
|
||||
{createdAt && (
|
||||
<div className="text-sm text-white/60">
|
||||
Created: {dayjs(createdAt).format("MMM D, YYYY HH:mm:ss")}
|
||||
</div>
|
||||
)}
|
||||
{additionalInfo}
|
||||
</div>
|
||||
|
||||
{/* buttons with spacing */}
|
||||
{(onAddToSceneClicked && downloadUrl) || downloadUrl ? (
|
||||
<div className="mt-15 mb-15 flex justify-end gap-2">
|
||||
<Button
|
||||
onClick={async (e) => {
|
||||
//let _result = await FalHunyuanImageTo3d({
|
||||
// image_media_token: mediaId,
|
||||
// //base64_image: downloadUrl,
|
||||
//});
|
||||
let result = await EnqueueImageTo3dObject({
|
||||
image_media_token: mediaId,
|
||||
model: EnqueueImageTo3dObjectModel.Hunyuan3d2,
|
||||
});
|
||||
//e.stopPropagation();
|
||||
//await onAddToSceneClicked(downloadUrl, mediaId);
|
||||
//onClose(); // close the lightbox
|
||||
//onCloseGallery(); // close the gallery
|
||||
}}
|
||||
>
|
||||
3D
|
||||
</Button>
|
||||
|
||||
{onAddToSceneClicked && downloadUrl && (
|
||||
<Button
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
await onAddToSceneClicked(downloadUrl, mediaId);
|
||||
onClose(); // close the lightbox
|
||||
onCloseGallery(); // close the gallery
|
||||
}}
|
||||
>
|
||||
Add to Current Scene
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{downloadUrl &&
|
||||
(onDownloadClicked ? (
|
||||
<Button
|
||||
icon={faDownToLine}
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
await onDownloadClicked(downloadUrl);
|
||||
}}
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
) : (
|
||||
<a
|
||||
href={downloadUrl}
|
||||
download
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="no-underline"
|
||||
>
|
||||
<Button icon={faDownToLine}>Download</Button>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</Transition>,
|
||||
document.body
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,14 @@ export const ArtCraftSignUp = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputFocus = () => {
|
||||
|
||||
};
|
||||
|
||||
const handleInputBlur = () => {
|
||||
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<h2 className="text-3xl font-bold mb-3 text-center">
|
||||
@@ -88,6 +96,8 @@ export const ArtCraftSignUp = ({
|
||||
placeholder="Username"
|
||||
required
|
||||
autoComplete="off"
|
||||
onFocus={handleInputFocus}
|
||||
onBlur={handleInputBlur}
|
||||
/>
|
||||
<Input
|
||||
label="Email"
|
||||
@@ -98,6 +108,8 @@ export const ArtCraftSignUp = ({
|
||||
required={isSignUp}
|
||||
style={isSignUp ? {} : { display: "none" }}
|
||||
autoComplete="off"
|
||||
onFocus={handleInputFocus}
|
||||
onBlur={handleInputBlur}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
@@ -108,6 +120,8 @@ export const ArtCraftSignUp = ({
|
||||
placeholder="Email or Username"
|
||||
required
|
||||
autoComplete="off"
|
||||
onFocus={handleInputFocus}
|
||||
onBlur={handleInputBlur}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -119,6 +133,8 @@ export const ArtCraftSignUp = ({
|
||||
placeholder="Password"
|
||||
required
|
||||
autoComplete="off"
|
||||
onFocus={handleInputFocus}
|
||||
onBlur={handleInputBlur}
|
||||
/>
|
||||
{isSignUp && (
|
||||
<Input
|
||||
@@ -129,6 +145,8 @@ export const ArtCraftSignUp = ({
|
||||
placeholder="Confirm Password"
|
||||
required
|
||||
autoComplete="off"
|
||||
onFocus={handleInputFocus}
|
||||
onBlur={handleInputBlur}
|
||||
/>
|
||||
)}
|
||||
<button type="submit" className="hidden" />
|
||||
|
||||
@@ -23,6 +23,7 @@ interface LoginModalProps {
|
||||
openAiLogo?: string;
|
||||
onOpenChange?: (isOpen: boolean) => void;
|
||||
onArtCraftAuthSuccess?: (userInfo: any) => void;
|
||||
isSignUp?: boolean;
|
||||
}
|
||||
|
||||
export function LoginModal({
|
||||
@@ -32,35 +33,21 @@ export function LoginModal({
|
||||
openAiLogo,
|
||||
onOpenChange,
|
||||
onArtCraftAuthSuccess,
|
||||
isSignUp: initialIsSignUp = true,
|
||||
}: LoginModalProps) {
|
||||
const [step, setStep] = useState(1);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isLoggedInArtCraft, setIsLoggedInArtCraft] = useState(false);
|
||||
const [isSignUp, setIsSignUp] = useState(true);
|
||||
const [isSignUp, setIsSignUp] = useState(initialIsSignUp);
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const artCraftFormRef = useRef<HTMLFormElement>(null);
|
||||
const [initialArtCraftLoginStatusForUI, setInitialArtCraftLoginStatusForUI] =
|
||||
useState<boolean | null>(null);
|
||||
const [initialSoraSessionStatusForUI, setInitialSoraSessionStatusForUI] =
|
||||
useState<boolean | null>(null);
|
||||
const [showDiscord, setShowDiscord] = useState(false);
|
||||
const [showSuccess, setShowSuccess] = useState(false);
|
||||
|
||||
// Determine total steps and current step for UI
|
||||
let uiTotalSteps = 4;
|
||||
let uiCurrentStep = step;
|
||||
if (initialSoraSessionStatusForUI) {
|
||||
// If Sora session exists, remove the Sora step (step 3)
|
||||
uiTotalSteps = 3;
|
||||
if (step === 1) uiCurrentStep = 1;
|
||||
else if (step === 2) uiCurrentStep = 2;
|
||||
else if (step === 4) uiCurrentStep = 3;
|
||||
} else if (initialArtCraftLoginStatusForUI) {
|
||||
// If ArtCraft is logged in but not Sora, keep 4 steps
|
||||
uiTotalSteps = 4;
|
||||
if (step === 1) uiCurrentStep = 1;
|
||||
else if (step === 3) uiCurrentStep = 2;
|
||||
else if (step === 4) uiCurrentStep = 3;
|
||||
}
|
||||
// Now we have 3 steps: Welcome, Sign up/Login, and Success
|
||||
const uiTotalSteps = 3;
|
||||
const uiCurrentStep = showSuccess ? 3 : showDiscord ? 2 : step;
|
||||
|
||||
const initSession = async () => {
|
||||
const result = await CheckSoraSession();
|
||||
@@ -82,34 +69,15 @@ export function LoginModal({
|
||||
|
||||
// Check session on component mount
|
||||
useEffect(() => {
|
||||
// Run both checks in parallel
|
||||
Promise.all([initSession(), checkArtCraftLogin()]).then(
|
||||
([soraSessionExists, loggedIn]) => {
|
||||
setInitialArtCraftLoginStatusForUI(loggedIn || false); // Set initial status for UI step counting
|
||||
setInitialSoraSessionStatusForUI(soraSessionExists || false); // Set initial Sora status for UI step counting
|
||||
|
||||
if (soraSessionExists && loggedIn) {
|
||||
setIsLoggedInArtCraft(true);
|
||||
setIsOpen(false); // Close the modal if fully logged in
|
||||
return;
|
||||
} else if (soraSessionExists && !loggedIn) {
|
||||
// Sora session exists, but not logged in to ArtCraft
|
||||
setIsOpen(true);
|
||||
setStep(2);
|
||||
setIsLoggedInArtCraft(false);
|
||||
} else if (!soraSessionExists && loggedIn) {
|
||||
// No Sora session, but logged in to ArtCraft
|
||||
setIsOpen(true);
|
||||
setStep(3);
|
||||
setIsLoggedInArtCraft(true);
|
||||
} else if (!soraSessionExists) {
|
||||
// No Sora session, start at step 1
|
||||
setIsOpen(true);
|
||||
setStep(1);
|
||||
setIsLoggedInArtCraft(false); // Ensure this is also set
|
||||
}
|
||||
checkArtCraftLogin().then((loggedIn) => {
|
||||
if (loggedIn) {
|
||||
setIsLoggedInArtCraft(true);
|
||||
setIsOpen(false);
|
||||
} else {
|
||||
setIsOpen(true);
|
||||
setStep(1); // Always start at step 1
|
||||
}
|
||||
);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -122,31 +90,7 @@ export function LoginModal({
|
||||
};
|
||||
|
||||
const handleNext = async () => {
|
||||
if (step === 3) {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
if (IsDesktopApp()) {
|
||||
await invoke("open_sora_login_command");
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
const result = await CheckSoraSession();
|
||||
const sessionExists = result.state === SoraSessionState.Valid;
|
||||
if (sessionExists) {
|
||||
setStep(4); // Always go to the final step after Sora login
|
||||
}
|
||||
} else {
|
||||
alert("Please open the desktop app to login");
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
const sessionExists = true;
|
||||
if (sessionExists) {
|
||||
setStep(4); // Always go to the final step after Sora login
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Login failed:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
} else if (step === 2) {
|
||||
if (step === 2) {
|
||||
// Trigger the form submit in ArtCraftSignUp
|
||||
if (artCraftFormRef.current) {
|
||||
artCraftFormRef.current.requestSubmit();
|
||||
@@ -174,12 +118,68 @@ export function LoginModal({
|
||||
</div>
|
||||
);
|
||||
|
||||
const handleDiscordJoin = () => {
|
||||
window.open("https://discord.gg/75svZP2Vje", "_blank");
|
||||
setShowDiscord(false);
|
||||
setShowSuccess(true);
|
||||
};
|
||||
|
||||
// Step content rendering
|
||||
const renderStepContent = () => {
|
||||
if (step === 2 && isLoggedInArtCraft) {
|
||||
setStep(3);
|
||||
return null;
|
||||
if (showSuccess) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<h2 className="text-3xl font-bold mb-2 text-center">
|
||||
Thank you for signing in!
|
||||
</h2>
|
||||
<p className="text-white/70 mb-6 text-center">
|
||||
You're all set to start creating amazing content.
|
||||
</p>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleClose}
|
||||
icon={faArrowRight}
|
||||
iconFlip={true}
|
||||
className="text-md"
|
||||
>
|
||||
Get Started
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (showDiscord) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<h2 className="text-3xl font-bold mb-2 text-center">
|
||||
Join Our Community
|
||||
</h2>
|
||||
<p className="text-white/70 mb-6 text-center px-24">
|
||||
Connect with other creators, share your work, and get the latest updates in our Discord community.
|
||||
</p>
|
||||
<div className="flex gap-4">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setShowDiscord(false);
|
||||
setShowSuccess(true);
|
||||
}}
|
||||
>
|
||||
Skip for now
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleDiscordJoin}
|
||||
icon={faDiscord}
|
||||
className="text-md bg-[#5865F2] hover:bg-[#6a76ff]"
|
||||
>
|
||||
Join Discord
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
switch (step) {
|
||||
case 1:
|
||||
return (
|
||||
@@ -240,12 +240,15 @@ export function LoginModal({
|
||||
try {
|
||||
let signupResponse, loginResponse;
|
||||
if (isSignUp) {
|
||||
console.log("Sign up!")
|
||||
signupResponse = await usersApi.Signup({
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
passwordConfirmation,
|
||||
});
|
||||
console.log(signupResponse)
|
||||
|
||||
if (!signupResponse.success) {
|
||||
setErrorMessage(
|
||||
signupResponse.errorMessage ||
|
||||
@@ -258,41 +261,32 @@ export function LoginModal({
|
||||
usernameOrEmail: username || email,
|
||||
password,
|
||||
});
|
||||
if (!loginResponse.success) {
|
||||
setErrorMessage(
|
||||
loginResponse.errorMessage ||
|
||||
"Login after signup failed, please try again. "
|
||||
);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
loginResponse = await usersApi.Login({
|
||||
usernameOrEmail: username || email,
|
||||
password,
|
||||
});
|
||||
if (!loginResponse.success) {
|
||||
setErrorMessage(
|
||||
loginResponse.errorMessage ||
|
||||
"Login failed, please try again."
|
||||
);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!loginResponse.success) {
|
||||
setErrorMessage(
|
||||
loginResponse.errorMessage ||
|
||||
"Login failed, please try again."
|
||||
);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoggedInArtCraft(true);
|
||||
console.log("ARTCRAFT LOGIN SUCCeSS")
|
||||
if (onArtCraftAuthSuccess) {
|
||||
const session = await usersApi.GetSession();
|
||||
const userInfo = session.data?.user;
|
||||
console.log("USERINFO")
|
||||
console.log(userInfo)
|
||||
if (userInfo) onArtCraftAuthSuccess(userInfo);
|
||||
}
|
||||
// Check for Sora session and skip to step 4 if present
|
||||
const soraSessionExists = await initSession();
|
||||
if (soraSessionExists) {
|
||||
setStep(4);
|
||||
} else {
|
||||
setStep(3);
|
||||
}
|
||||
setShowDiscord(true); // Show Discord step after successful login
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setErrorMessage(
|
||||
@@ -308,53 +302,40 @@ export function LoginModal({
|
||||
errorMessage={errorMessage}
|
||||
/>
|
||||
);
|
||||
case 3:
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<h2 className="text-3xl font-bold mb-2 text-center">
|
||||
Login with your OpenAI account
|
||||
</h2>
|
||||
<p className="text-white/70 mb-6 text-center px-16">
|
||||
We're the application layer for images and video. To provide you
|
||||
with <b>100% free service</b>, we use your existing AI accounts.
|
||||
To start off, you'll need to add your OpenAI Sora account. If you
|
||||
have a ChatGPT subscription, you can log in with Sora.
|
||||
</p>
|
||||
<img
|
||||
src={openAiLogo}
|
||||
alt="OpenAI Logo"
|
||||
className="w-72 h-72 mx-auto grow"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case 4:
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<h2 className="text-3xl font-bold mb-2 text-center">
|
||||
Thank you for signing in!
|
||||
</h2>
|
||||
<p className="text-white/70 mb-6 text-center px-24">
|
||||
You're all set to start creating amazing content. Join our Discord
|
||||
community to connect with other creators, share your work, and get
|
||||
the latest updates.
|
||||
</p>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() =>
|
||||
window.open("https://discord.gg/75svZP2Vje", "_blank")
|
||||
}
|
||||
icon={faDiscord}
|
||||
className="text-md bg-[#5865F2] hover:bg-[#6a76ff] rounded-xl"
|
||||
>
|
||||
Join our Discord
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const renderFooterButtons = () => {
|
||||
if (showSuccess || showDiscord) {
|
||||
return null; // No footer buttons needed in success or discord states
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-end justify-center gap-2.5 mt-10 grow">
|
||||
{step === 2 && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleBack}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
icon={faArrowRight}
|
||||
iconFlip={true}
|
||||
onClick={handleNext}
|
||||
loading={isLoading}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{step === 1 ? "Continue" : isSignUp ? "Sign up" : "Login"}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return createPortal(
|
||||
<Transition appear show={isOpen}>
|
||||
<div className="fixed inset-0 z-[100]">
|
||||
@@ -382,58 +363,16 @@ export function LoginModal({
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex flex-col gap-4 p-8 h-full">
|
||||
<span className="text-sm text-center opacity-60 font-medium">
|
||||
Step {uiCurrentStep} of {uiTotalSteps}
|
||||
</span>
|
||||
{renderProgress()}
|
||||
{!showSuccess && (
|
||||
<>
|
||||
<span className="text-sm text-center opacity-60 font-medium">
|
||||
Step {uiCurrentStep} of {uiTotalSteps}
|
||||
</span>
|
||||
{renderProgress()}
|
||||
</>
|
||||
)}
|
||||
{renderStepContent()}
|
||||
<div className="flex items-end justify-center gap-2.5 mt-10 grow">
|
||||
{step === 2 ? (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleBack}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
) : null}
|
||||
{step === 3 && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleClose}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Skip for now
|
||||
</Button>
|
||||
)}
|
||||
{step === 4 ? (
|
||||
<Button
|
||||
icon={faArrowRight}
|
||||
iconFlip={true}
|
||||
onClick={handleClose}
|
||||
>
|
||||
Start Creating Now
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
icon={step === 3 ? faRightToBracket : faArrowRight}
|
||||
iconFlip={step !== 3}
|
||||
onClick={handleNext}
|
||||
loading={isLoading}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{step === 1
|
||||
? "Continue"
|
||||
: step === 2
|
||||
? isSignUp
|
||||
? "Sign up"
|
||||
: "Login"
|
||||
: "Login with OpenAI"}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{renderFooterButtons()}
|
||||
</div>
|
||||
</div>
|
||||
</TransitionChild>
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
* e.g.
|
||||
* .container {
|
||||
* }
|
||||
*/
|
||||
*/
|
||||
|
||||
@@ -18,6 +18,49 @@ import {
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { DomLevels } from "@storyteller/common";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GLOBAL inert / aria-hidden stripper – applies once per page load
|
||||
// Overrides the inert attribute set by Headless UI to allow background interaction / stacked modals
|
||||
// ---------------------------------------------------------------------------
|
||||
declare global {
|
||||
interface Window {
|
||||
__inertStripperInstalled?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
if (!window.__inertStripperInstalled) {
|
||||
window.__inertStripperInstalled = true;
|
||||
|
||||
const strip = (el: Element) => {
|
||||
if (el.hasAttribute("inert")) el.removeAttribute("inert");
|
||||
// @ts-ignore – some browsers expose .inert as a property
|
||||
if ((el as any).inert) (el as any).inert = false;
|
||||
if (el.getAttribute("aria-hidden") === "true")
|
||||
el.removeAttribute("aria-hidden");
|
||||
};
|
||||
|
||||
// 1. Clean anything that already exists
|
||||
document.querySelectorAll("[inert]").forEach(strip);
|
||||
|
||||
// 2. Keep document clean forever
|
||||
const inertObserver = new MutationObserver((mutations) => {
|
||||
for (const m of mutations) {
|
||||
if (m.type === "attributes" && m.attributeName === "inert") {
|
||||
strip(m.target as Element);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
inertObserver.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
subtree: true,
|
||||
attributeFilter: ["inert"],
|
||||
});
|
||||
}
|
||||
|
||||
// Simple global z-index tracker for stacked modals
|
||||
let modalZCounter = 70;
|
||||
|
||||
const DialogBackdrop = ({
|
||||
className,
|
||||
onClose,
|
||||
@@ -120,8 +163,8 @@ export const Modal = ({
|
||||
titleIcon,
|
||||
onTitleIconClick,
|
||||
onClose,
|
||||
disableHotkeyInput = () => {},
|
||||
enableHotkeyInput = () => {},
|
||||
disableHotkeyInput = () => {},
|
||||
className,
|
||||
backdropClassName,
|
||||
width,
|
||||
@@ -130,6 +173,7 @@ export const Modal = ({
|
||||
titleIconClassName,
|
||||
showClose = true,
|
||||
draggable = false,
|
||||
resizable = false,
|
||||
initialPosition,
|
||||
closeOnOutsideClick = true,
|
||||
allowBackgroundInteraction = false,
|
||||
@@ -148,6 +192,7 @@ export const Modal = ({
|
||||
childPadding?: boolean;
|
||||
showClose?: boolean;
|
||||
draggable?: boolean;
|
||||
resizable?: boolean;
|
||||
disableHotkeyInput?: (level: number) => void;
|
||||
enableHotkeyInput?: (level: number) => void;
|
||||
/**
|
||||
@@ -172,11 +217,13 @@ export const Modal = ({
|
||||
null
|
||||
);
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const [resizing, setResizing] = useState(false);
|
||||
const dragStart = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
|
||||
const mouseStart = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
const positionRef = useRef<{ x: number; y: number } | null>(null);
|
||||
const lastPositionRef = useRef<{ x: number; y: number } | null>(null);
|
||||
const [zIndex, setZIndex] = useState<number>(() => ++modalZCounter);
|
||||
|
||||
// Expanded state
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
@@ -197,7 +244,7 @@ export const Modal = ({
|
||||
modalRef.current.style.top = "0px";
|
||||
modalRef.current.style.margin = "0";
|
||||
modalRef.current.style.position = "fixed";
|
||||
modalRef.current.style.zIndex = "70";
|
||||
modalRef.current.style.zIndex = String(zIndex);
|
||||
}
|
||||
} else {
|
||||
// Restore last non-expanded position
|
||||
@@ -210,12 +257,12 @@ export const Modal = ({
|
||||
modalRef.current.style.top = lastNonExpandedPosition.current.y + "px";
|
||||
modalRef.current.style.margin = "0";
|
||||
modalRef.current.style.position = "fixed";
|
||||
modalRef.current.style.zIndex = "70";
|
||||
modalRef.current.style.zIndex = String(zIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [expanded]);
|
||||
}, [expanded, zIndex]);
|
||||
const toggleExpanded = () => setExpanded((v) => !v);
|
||||
|
||||
// Reset position when modal is closed or opened
|
||||
@@ -237,7 +284,7 @@ export const Modal = ({
|
||||
modalRef.current.style.top = lastPositionRef.current.y + "px";
|
||||
modalRef.current.style.margin = "0";
|
||||
modalRef.current.style.position = "fixed";
|
||||
modalRef.current.style.zIndex = "70";
|
||||
modalRef.current.style.zIndex = String(zIndex);
|
||||
}
|
||||
} else if (initialPosition) {
|
||||
setPosition({ ...initialPosition });
|
||||
@@ -247,11 +294,11 @@ export const Modal = ({
|
||||
modalRef.current.style.top = initialPosition.y + "px";
|
||||
modalRef.current.style.margin = "0";
|
||||
modalRef.current.style.position = "fixed";
|
||||
modalRef.current.style.zIndex = "70";
|
||||
modalRef.current.style.zIndex = String(zIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [isOpen]);
|
||||
}, [isOpen, zIndex]);
|
||||
|
||||
// Handle mouse move and up events
|
||||
useEffect(() => {
|
||||
@@ -284,7 +331,7 @@ export const Modal = ({
|
||||
modalRef.current.style.top = positionRef.current.y + "px";
|
||||
modalRef.current.style.margin = "0";
|
||||
modalRef.current.style.position = "fixed";
|
||||
modalRef.current.style.zIndex = "70";
|
||||
modalRef.current.style.zIndex = String(zIndex);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -301,20 +348,24 @@ export const Modal = ({
|
||||
window.removeEventListener("mouseup", handleMouseUp);
|
||||
if (animationFrame) cancelAnimationFrame(animationFrame);
|
||||
};
|
||||
}, [dragging]);
|
||||
}, [dragging, zIndex]);
|
||||
|
||||
// Center modal if not being dragged
|
||||
const getModalStyle = (): React.CSSProperties => {
|
||||
if (!draggable || !position) {
|
||||
// If allowBackgroundInteraction, set pointerEvents: 'auto' for modal
|
||||
return allowBackgroundInteraction ? { pointerEvents: "auto" } : {};
|
||||
return {
|
||||
...(size ? { width: size.width, height: size.height } : {}),
|
||||
...(allowBackgroundInteraction ? { pointerEvents: "auto" } : {}),
|
||||
};
|
||||
}
|
||||
return {
|
||||
position: "fixed",
|
||||
left: position.x,
|
||||
top: position.y,
|
||||
margin: 0,
|
||||
zIndex: 70,
|
||||
zIndex,
|
||||
...(size ? { width: size.width, height: size.height } : {}),
|
||||
...(allowBackgroundInteraction ? { pointerEvents: "auto" } : {}),
|
||||
};
|
||||
};
|
||||
@@ -352,7 +403,7 @@ export const Modal = ({
|
||||
modalRef.current.style.top = y + "px";
|
||||
modalRef.current.style.margin = "0";
|
||||
modalRef.current.style.position = "fixed";
|
||||
modalRef.current.style.zIndex = "70";
|
||||
modalRef.current.style.zIndex = String(zIndex);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -407,12 +458,301 @@ export const Modal = ({
|
||||
: children;
|
||||
}
|
||||
|
||||
/**
|
||||
* ------------------ RESIZING LOGIC --------------------
|
||||
*/
|
||||
const resizeDirRef = useRef<string | null>(null);
|
||||
const resizeStart = useRef<{
|
||||
mouseX: number;
|
||||
mouseY: number;
|
||||
width: number;
|
||||
height: number;
|
||||
x: number;
|
||||
y: number;
|
||||
} | null>(null);
|
||||
|
||||
const handleResizeStart = (e: React.MouseEvent, dir: string) => {
|
||||
if (!modalRef.current) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (expanded) return; // don't allow resize when expanded
|
||||
const rect = modalRef.current.getBoundingClientRect();
|
||||
resizeDirRef.current = dir;
|
||||
resizeStart.current = {
|
||||
mouseX: e.clientX,
|
||||
mouseY: e.clientY,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
x: rect.left,
|
||||
y: rect.top,
|
||||
};
|
||||
setResizing(true);
|
||||
};
|
||||
|
||||
// Handle resizing mouse move / up
|
||||
useEffect(() => {
|
||||
if (!resizing) return;
|
||||
let animationFrame: number | null = null;
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!modalRef.current || !resizeStart.current || !resizeDirRef.current)
|
||||
return;
|
||||
const dx = e.clientX - resizeStart.current.mouseX;
|
||||
const dy = e.clientY - resizeStart.current.mouseY;
|
||||
|
||||
let newWidth = resizeStart.current.width;
|
||||
let newHeight = resizeStart.current.height;
|
||||
let newX = resizeStart.current.x;
|
||||
let newY = resizeStart.current.y;
|
||||
|
||||
const dir = resizeDirRef.current;
|
||||
|
||||
const minWidth = 320;
|
||||
const minHeight = 240;
|
||||
|
||||
if (dir.includes("right")) {
|
||||
newWidth = resizeStart.current.width + dx;
|
||||
}
|
||||
if (dir.includes("left")) {
|
||||
newWidth = resizeStart.current.width - dx;
|
||||
newX = resizeStart.current.x + dx;
|
||||
}
|
||||
if (dir.includes("bottom")) {
|
||||
newHeight = resizeStart.current.height + dy;
|
||||
}
|
||||
if (dir.includes("top")) {
|
||||
newHeight = resizeStart.current.height - dy;
|
||||
newY = resizeStart.current.y + dy;
|
||||
}
|
||||
|
||||
newWidth = Math.max(minWidth, newWidth);
|
||||
newHeight = Math.max(minHeight, newHeight);
|
||||
|
||||
positionRef.current = { x: newX, y: newY };
|
||||
sizeRef.current = { width: newWidth, height: newHeight };
|
||||
|
||||
if (animationFrame) cancelAnimationFrame(animationFrame);
|
||||
animationFrame = requestAnimationFrame(() => {
|
||||
if (modalRef.current && positionRef.current) {
|
||||
modalRef.current.style.width = newWidth + "px";
|
||||
modalRef.current.style.height = newHeight + "px";
|
||||
modalRef.current.style.left = positionRef.current.x + "px";
|
||||
modalRef.current.style.top = positionRef.current.y + "px";
|
||||
modalRef.current.style.margin = "0";
|
||||
modalRef.current.style.position = "fixed";
|
||||
modalRef.current.style.zIndex = String(zIndex);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setResizing(false);
|
||||
if (positionRef.current) setPosition({ ...positionRef.current });
|
||||
if (sizeRef.current) setSize({ ...sizeRef.current });
|
||||
};
|
||||
|
||||
window.addEventListener("mousemove", handleMouseMove);
|
||||
window.addEventListener("mouseup", handleMouseUp);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("mousemove", handleMouseMove);
|
||||
window.removeEventListener("mouseup", handleMouseUp);
|
||||
if (animationFrame) cancelAnimationFrame(animationFrame);
|
||||
};
|
||||
}, [resizing, zIndex]);
|
||||
|
||||
// Render resize handles if resizable and not expanded
|
||||
const renderResizeHandles = () => {
|
||||
if (!resizable || expanded) return null;
|
||||
const baseClass = "absolute z-[75] bg-transparent select-none";
|
||||
const handleSize = 5; // px
|
||||
const sideThickness = 2; // for edge handles
|
||||
const handles = [
|
||||
{ dir: "top", className: `top-0 left-0 w-full h-${sideThickness}` },
|
||||
{ dir: "bottom", className: `bottom-0 left-0 w-full h-${sideThickness}` },
|
||||
{ dir: "left", className: `left-0 top-0 h-full w-${sideThickness}` },
|
||||
{ dir: "right", className: `right-0 top-0 h-full w-${sideThickness}` },
|
||||
{
|
||||
dir: "top-left",
|
||||
className: `top-0 left-0 w-${handleSize} h-${handleSize}`,
|
||||
},
|
||||
{
|
||||
dir: "top-right",
|
||||
className: `top-0 right-0 w-${handleSize} h-${handleSize}`,
|
||||
},
|
||||
{
|
||||
dir: "bottom-left",
|
||||
className: `bottom-0 left-0 w-${handleSize} h-${handleSize}`,
|
||||
},
|
||||
{
|
||||
dir: "bottom-right",
|
||||
className: `bottom-0 right-0 w-${handleSize} h-${handleSize}`,
|
||||
},
|
||||
];
|
||||
|
||||
const cursorMap: Record<string, string> = {
|
||||
top: "n-resize",
|
||||
bottom: "s-resize",
|
||||
left: "w-resize",
|
||||
right: "e-resize",
|
||||
"top-left": "nw-resize",
|
||||
"top-right": "ne-resize",
|
||||
"bottom-left": "sw-resize",
|
||||
"bottom-right": "se-resize",
|
||||
};
|
||||
|
||||
return handles.map((h) => (
|
||||
<div
|
||||
key={h.dir}
|
||||
className={`${baseClass} ${h.className}`}
|
||||
style={{ cursor: cursorMap[h.dir] }}
|
||||
onMouseDown={(e) => handleResizeStart(e, h.dir)}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
// Size (width & height) state for resizable modal
|
||||
const [size, setSize] = useState<{ width: number; height: number } | null>(
|
||||
null
|
||||
);
|
||||
const sizeRef = useRef<{ width: number; height: number } | null>(null);
|
||||
const lastSizeRef = useRef<{ width: number; height: number } | null>(null);
|
||||
|
||||
// Capture initial size on first open
|
||||
useEffect(() => {
|
||||
if (isOpen && !size && modalRef.current) {
|
||||
const { width, height } = modalRef.current.getBoundingClientRect();
|
||||
setSize({ width, height });
|
||||
sizeRef.current = { width, height };
|
||||
}
|
||||
}, [isOpen, size]);
|
||||
|
||||
// Persist size across close / reopen
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
if (sizeRef.current) lastSizeRef.current = { ...sizeRef.current };
|
||||
} else {
|
||||
if (lastSizeRef.current && modalRef.current) {
|
||||
const { width, height } = lastSizeRef.current;
|
||||
modalRef.current.style.width = width + "px";
|
||||
modalRef.current.style.height = height + "px";
|
||||
setSize({ width, height });
|
||||
sizeRef.current = { width, height };
|
||||
}
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
// Bring to front when user interacts with modal (mouse down anywhere inside)
|
||||
useEffect(() => {
|
||||
const handleBringToFront = () => {
|
||||
if (modalRef.current) {
|
||||
if (zIndex < modalZCounter) {
|
||||
modalZCounter += 1;
|
||||
setZIndex(modalZCounter);
|
||||
modalRef.current.style.zIndex = String(modalZCounter);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const node = modalRef.current;
|
||||
if (node) {
|
||||
node.addEventListener("mousedown", handleBringToFront);
|
||||
}
|
||||
return () => {
|
||||
if (node) node.removeEventListener("mousedown", handleBringToFront);
|
||||
};
|
||||
}, [zIndex]);
|
||||
|
||||
// If background interaction is allowed, ensure this modal (and its ancestors)
|
||||
// never get the "inert" attribute Headless-UI uses to lock background dialogs.
|
||||
useEffect(() => {
|
||||
if (!allowBackgroundInteraction) return;
|
||||
const node = modalRef.current as HTMLElement | null;
|
||||
if (!node) return;
|
||||
|
||||
const stripInert = (el: HTMLElement | null) => {
|
||||
if (!el) return;
|
||||
if (el.hasAttribute("inert")) el.removeAttribute("inert");
|
||||
// Also clear the property for browsers implementing it as mutable prop
|
||||
// @ts-ignore
|
||||
if ((el as any).inert) (el as any).inert = false;
|
||||
if (el.getAttribute("aria-hidden") === "true") {
|
||||
el.removeAttribute("aria-hidden");
|
||||
}
|
||||
};
|
||||
|
||||
// Remove inert from this modal and all its ancestors (Headless UI sets it on the modal container)
|
||||
let cur: HTMLElement | null = node;
|
||||
while (cur) {
|
||||
stripInert(cur);
|
||||
cur = cur.parentElement as HTMLElement | null;
|
||||
}
|
||||
|
||||
// MutationObserver to keep stripping inert from THIS modal element if reapplied
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((m) => {
|
||||
if (
|
||||
m.type === "attributes" &&
|
||||
m.attributeName === "inert" &&
|
||||
m.target instanceof HTMLElement
|
||||
) {
|
||||
const target = m.target as HTMLElement;
|
||||
// Only touch if the target is this modal or one of its ancestors
|
||||
if (target === node || node.contains(target)) {
|
||||
stripInert(target);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
observer.observe(node, {
|
||||
attributes: true,
|
||||
subtree: false,
|
||||
attributeFilter: ["inert"],
|
||||
});
|
||||
|
||||
// Additionally, Headless UI adds inert to previous portal containers that are
|
||||
// siblings of the one just created. We strip inert from ANY portal container
|
||||
// so long as background interaction is requested.
|
||||
const stripInertFromPortals = () => {
|
||||
const portals = document.querySelectorAll(
|
||||
"[data-headlessui-portal][inert]"
|
||||
);
|
||||
portals.forEach((el) => el.removeAttribute("inert"));
|
||||
};
|
||||
|
||||
stripInertFromPortals();
|
||||
|
||||
const globalObserver = new MutationObserver((mutList) => {
|
||||
mutList.forEach((m) => {
|
||||
if (
|
||||
m.type === "attributes" &&
|
||||
m.attributeName === "inert" &&
|
||||
(m.target as HTMLElement).hasAttribute("inert")
|
||||
) {
|
||||
const target = m.target as HTMLElement;
|
||||
if (target.hasAttribute("data-headlessui-portal")) {
|
||||
stripInert(target as HTMLElement);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
globalObserver.observe(document.body, {
|
||||
attributes: true,
|
||||
subtree: true,
|
||||
attributeFilter: ["inert"],
|
||||
});
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
globalObserver.disconnect();
|
||||
};
|
||||
}, [allowBackgroundInteraction]);
|
||||
|
||||
return (
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-[70]"
|
||||
onClose={onClose}
|
||||
onClose={closeOnOutsideClick ? onClose : () => null}
|
||||
static={allowBackgroundInteraction}
|
||||
>
|
||||
<div
|
||||
@@ -424,7 +764,10 @@ export const Modal = ({
|
||||
{/* Backdrop always rendered first in stacking context */}
|
||||
{!allowBackgroundInteraction && (
|
||||
<DialogBackdrop
|
||||
className={backdropClassName}
|
||||
className={twMerge(
|
||||
allowBackgroundInteraction ? "pointer-events-none" : "",
|
||||
backdropClassName
|
||||
)}
|
||||
onClose={onClose}
|
||||
closeOnOutsideClick={closeOnOutsideClick}
|
||||
disableHotkeyInput={disableHotkeyInput}
|
||||
@@ -438,7 +781,14 @@ export const Modal = ({
|
||||
/>
|
||||
)}
|
||||
<ModalExpandContext.Provider value={{ expanded, toggleExpanded }}>
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<div
|
||||
className="flex min-h-full items-center justify-center p-4 text-center"
|
||||
style={
|
||||
allowBackgroundInteraction
|
||||
? { pointerEvents: "none" }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<TransitionChild
|
||||
as="div"
|
||||
enter="ease-out duration-200"
|
||||
@@ -451,7 +801,9 @@ export const Modal = ({
|
||||
"w-full max-w-lg transform rounded-xl relative border border-ui-panel-border bg-[#2C2C2C] text-left align-middle shadow-2xl z-[70]",
|
||||
childPadding && !expanded ? "p-4" : "",
|
||||
className,
|
||||
dragging && !expanded ? "!transition-none" : "transition-all",
|
||||
(dragging || resizing) && !expanded
|
||||
? "!transition-none"
|
||||
: "transition-all",
|
||||
expanded &&
|
||||
"w-screen h-screen max-w-screen max-h-screen rounded-none"
|
||||
)}
|
||||
@@ -495,9 +847,11 @@ export const Modal = ({
|
||||
</DialogTitle>
|
||||
)}
|
||||
<div className={`h-full`.trim()}>{enhancedChildren}</div>
|
||||
{/* resize handles inside panel so clicks don't count as outside */}
|
||||
{renderResizeHandles()}
|
||||
</DialogPanel>
|
||||
{showClose && (
|
||||
<div className="absolute top-0 right-0 p-2.5">
|
||||
<div className="absolute top-0 right-0 m-2.5 z-[80]">
|
||||
<CloseButton onClick={onClose} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
+2
-3
@@ -5,6 +5,7 @@ import { SoraAccountButton } from "./SoraAccountButton";
|
||||
import { SoraAccountBlock } from "./SoraAccountBlock";
|
||||
import { ArtcraftAccountBlock } from "./ArtcraftAccountBlock";
|
||||
import { FalApiKeyBlock } from "./FalApiKeyBlock";
|
||||
import { OpenAIApiKeyBlock } from "./OpenAIKeyBlock";
|
||||
|
||||
interface AccountInfo {
|
||||
username: string;
|
||||
@@ -57,11 +58,9 @@ export const AccountSettingsPane = ({
|
||||
*/}
|
||||
|
||||
<ArtcraftAccountBlock globalAccountLogoutCallback={globalAccountLogoutCallback}/>
|
||||
|
||||
<SoraAccountBlock />
|
||||
|
||||
<FalApiKeyBlock />
|
||||
|
||||
<OpenAIApiKeyBlock/>
|
||||
{/*
|
||||
<hr />
|
||||
|
||||
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { GetOpenAIApiKey, SetOpenAIApiKey } from "@storyteller/tauri-api";
|
||||
import { Input } from "@storyteller/ui-input";
|
||||
|
||||
|
||||
export const OpenAIApiKeyBlock = () => {
|
||||
const [openAIApiKey, setOpenAIApiKey] = useState<string | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
const [isCheckingOpenAIApiKey, setIsCheckingOpenAIApiKey] =
|
||||
useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchKey = async () => {
|
||||
setIsCheckingOpenAIApiKey(true);
|
||||
try {
|
||||
const result = await GetOpenAIApiKey();
|
||||
if ("payload" in result) {
|
||||
setOpenAIApiKey(result.payload.key);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error fetching Fal API key", e);
|
||||
setOpenAIApiKey(undefined);
|
||||
} finally {
|
||||
setIsCheckingOpenAIApiKey(false);
|
||||
}
|
||||
};
|
||||
fetchKey();
|
||||
}, []);
|
||||
|
||||
const syncOpenAIApiKey = async (key: string) => {
|
||||
await SetOpenAIApiKey(key);
|
||||
setOpenAIApiKey(key);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex justify-between items-center">
|
||||
<span>Open AI API Key:</span>
|
||||
<Input
|
||||
value={openAIApiKey}
|
||||
onChange={(e) => syncOpenAIApiKey(e.target.value)}
|
||||
placeholder="Enter Open AI API key"
|
||||
/>
|
||||
|
||||
{/*<Button
|
||||
variant={
|
||||
isCheckingArtcraftSession
|
||||
? "secondary"
|
||||
: isLoggedIn
|
||||
? "destructive"
|
||||
: "primary"
|
||||
}
|
||||
className="h-[30px]"
|
||||
onClick={handleArtcraftButton}
|
||||
disabled={isCheckingArtcraftSession}
|
||||
>
|
||||
{isCheckingArtcraftSession ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faSpinnerThird}
|
||||
className="animate-spin text-sm"
|
||||
/>
|
||||
) : isLoggedIn ? (
|
||||
"Log Out"
|
||||
) : (
|
||||
"Log In"
|
||||
)}
|
||||
</Button>*/}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -6,6 +6,8 @@ export * from "./lib/fal/FalHunyuanImageTo3d.js";
|
||||
export * from "./lib/fal/FalKlingImageToVideo.js";
|
||||
export * from "./lib/fal/GetFalApiKey.js";
|
||||
export * from "./lib/fal/SetFalApiKey.js";
|
||||
export * from "./lib/openai/SetOpenAIApiKey.js";
|
||||
export * from "./lib/openai/GetOpenAIApiKey.js";
|
||||
export * from "./lib/settings/GetAppPreferences.js";
|
||||
export * from "./lib/settings/UpdateAppPreference.js";
|
||||
export * from "./lib/sora/CheckSoraSession.js";
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { CommandResult } from "../common/CommandStatus";
|
||||
|
||||
export interface GetOpenAIApiKeySuccessResult extends CommandResult {
|
||||
payload: {
|
||||
key: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type GetOpenAIApiKeyResult = GetOpenAIApiKeySuccessResult | CommandResult;
|
||||
|
||||
export const GetOpenAIApiKey = async (): Promise<GetOpenAIApiKeyResult> => {
|
||||
const result = await invoke("get_openai_api_key_command");
|
||||
return result as GetOpenAIApiKeyResult;
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { CommandResult } from "../common/CommandStatus";
|
||||
|
||||
export const SetOpenAIApiKey = async (key: string): Promise<CommandResult> => {
|
||||
const result = await invoke("set_openai_api_key_command", {
|
||||
request: {
|
||||
key: key,
|
||||
}
|
||||
});
|
||||
return result as CommandResult;
|
||||
};
|
||||
Reference in New Issue
Block a user