diff --git a/crates/schema/public/api/artcraft_api_defs/src/generate/object/generate_hunyuan_21_image_to_3d.rs b/crates/schema/public/api/artcraft_api_defs/src/generate/object/generate_hunyuan_21_image_to_3d.rs new file mode 100644 index 0000000000..7983a54422 --- /dev/null +++ b/crates/schema/public/api/artcraft_api_defs/src/generate/object/generate_hunyuan_21_image_to_3d.rs @@ -0,0 +1,22 @@ +use serde_derive::{Deserialize, Serialize}; +use tokens::tokens::generic_inference_jobs::InferenceJobToken; +use tokens::tokens::media_files::MediaFileToken; +use utoipa::ToSchema; + +pub const GENERATE_HUNYUAN_2_1_IMAGE_TO_3D_URL_PATH: &str = "/v1/generate/object/hunyuan_2.1_image_to_3d"; + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct GenerateHunyuan21ImageTo3dRequest { + /// Idempotency token to prevent duplicate requests. + pub uuid_idempotency_token: String, + + /// Source of the file to convert to 3D + /// It must be an image. + pub media_file_token: Option, +} + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct GenerateHunyuan21ImageTo3dResponse { + pub success: bool, + pub inference_job_token: InferenceJobToken, +} diff --git a/crates/schema/public/api/artcraft_api_defs/src/generate/object/mod.rs b/crates/schema/public/api/artcraft_api_defs/src/generate/object/mod.rs index 4f412d5a9c..0ef172ab4e 100644 --- a/crates/schema/public/api/artcraft_api_defs/src/generate/object/mod.rs +++ b/crates/schema/public/api/artcraft_api_defs/src/generate/object/mod.rs @@ -1 +1,2 @@ +pub mod generate_hunyuan_21_image_to_3d; pub mod generate_hunyuan_2_image_to_3d; \ No newline at end of file diff --git a/crates/service/web/storyteller_web/src/docs/api_doc.rs b/crates/service/web/storyteller_web/src/docs/api_doc.rs index c4050dd3a8..50942a364a 100644 --- a/crates/service/web/storyteller_web/src/docs/api_doc.rs +++ b/crates/service/web/storyteller_web/src/docs/api_doc.rs @@ -20,6 +20,8 @@ use artcraft_api_defs::generate::image::remove_image_background::RemoveImageBack use artcraft_api_defs::generate::image::remove_image_background::RemoveImageBackgroundResponse; 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::object::generate_hunyuan_21_image_to_3d::GenerateHunyuan21ImageTo3dRequest; +use artcraft_api_defs::generate::object::generate_hunyuan_21_image_to_3d::GenerateHunyuan21ImageTo3dResponse; 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::*; @@ -212,6 +214,8 @@ use crate::http_server::web_utils::response_success_helpers::*; 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::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, @@ -229,7 +233,6 @@ use crate::http_server::web_utils::response_success_helpers::*; crate::http_server::endpoints::media_files::get::get_media_file_handler::get_media_file_handler, crate::http_server::endpoints::media_files::list::list_featured_media_files_handler::list_featured_media_files_handler, crate::http_server::endpoints::media_files::list::list_media_files_by_batch_token_handler::list_media_files_by_batch_token_handler, - crate::http_server::endpoints::generate::object::generate_hunyuan2_image_to_3d_handler::generate_hunyuan_2_image_to_3d_handler, crate::http_server::endpoints::media_files::list::list_media_files_for_user_handler::list_media_files_for_user_handler, crate::http_server::endpoints::media_files::list::list_media_files_handler::list_media_files_handler, crate::http_server::endpoints::media_files::list::list_pinned_media_files_handler::list_pinned_media_files_handler, @@ -497,6 +500,8 @@ use crate::http_server::web_utils::response_success_helpers::*; GenerateFluxPro11UltraTextToImageNumImages, GenerateFluxPro11UltraTextToImageRequest, GenerateFluxPro11UltraTextToImageResponse, + GenerateHunyuan21ImageTo3dRequest, + GenerateHunyuan21ImageTo3dResponse, GenerateHunyuan2ImageTo3dRequest, GenerateHunyuan2ImageTo3dResponse, GetInferenceJobStatusError, diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/object/generate_hunyuan_21_image_to_3d_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/object/generate_hunyuan_21_image_to_3d_handler.rs new file mode 100644 index 0000000000..17f2e415f1 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/object/generate_hunyuan_21_image_to_3d_handler.rs @@ -0,0 +1,180 @@ +use std::fmt; +use std::sync::Arc; + +use crate::http_server::common_requests::media_file_token_path_info::MediaFileTokenPathInfo; +use crate::http_server::common_responses::common_web_error::CommonWebError; +use crate::http_server::common_responses::media::media_links::MediaLinks; +use crate::http_server::deprecated_endpoints::engine::create_scene_handler::CreateSceneError; +use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain; +use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError; +use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format; +use crate::http_server::web_utils::response_success_helpers::simple_json_success; +use crate::state::server_state::ServerState; +use crate::util::delete_role_disambiguation::{delete_role_disambiguation, DeleteRole}; +use actix_web::error::ResponseError; +use actix_web::http::StatusCode; +use actix_web::web::Json; +use actix_web::web::Path; +use actix_web::{web, HttpRequest, HttpResponse}; +use artcraft_api_defs::generate::object::generate_hunyuan_21_image_to_3d::GenerateHunyuan21ImageTo3dRequest; +use artcraft_api_defs::generate::object::generate_hunyuan_21_image_to_3d::GenerateHunyuan21ImageTo3dResponse; +use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath; +use enums::common::visibility::Visibility; +use fal_client::requests::webhook::object::enqueue_hunyuan_3d_21_image_to_3d_webhook::enqueue_hunyuan_3d_2_1_image_to_3d_webhook; +use fal_client::requests::webhook::object::enqueue_hunyuan_3d_21_image_to_3d_webhook::Hunyuan3d21Args; +use http_server_common::request::get_request_ip::get_request_ip; +use http_server_common::response::serialize_as_json_error::serialize_as_json_error; +use idempotency::uuid::generate_random_uuid; +use log::{error, info, warn}; +use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::insert_generic_inference_job_for_fal_queue; +use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory; +use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::InsertGenericInferenceForFalArgs; +use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token; +use mysql_queries::queries::media_files::get::get_media_file::{get_media_file, MediaFile}; +use tokens::tokens::media_files::MediaFileToken; +use utoipa::ToSchema; + +/// Hunyuan 2.1 Image to 3D +#[utoipa::path( + post, + tag = "Generate Objects", + path = "/v1/generate/object/hunyuan_2.1_image_to_3d", + responses( + (status = 200, description = "Success", body = GenerateHunyuan21ImageTo3dResponse), + ), + params( + ("request" = GenerateHunyuan21ImageTo3dRequest, description = "Payload for Request"), + ) +)] +pub async fn generate_hunyuan_21_image_to_3d_handler( + http_request: HttpRequest, + request: Json, + server_state: web::Data> +) -> Result, CommonWebError> { + let maybe_user_session = server_state + .session_checker + .maybe_get_user_session(&http_request, &server_state.mysql_pool) + .await + .map_err(|e| { + warn!("Session checker error: {:?}", e); + CommonWebError::ServerError + })?; + + let maybe_avt_token = server_state + .avt_cookie_manager + .get_avt_token_from_request(&http_request); + + // TODO: Limit usage for new accounts. Billing, free credits metering, etc. + + //let user_session = match maybe_user_session { + // Some(session) => session, + // None => { + // warn!("not logged in"); + // return Err(CommonWebError::NotAuthorized); + // } + //}; + + let media_file_token = match &request.media_file_token { + Some(token) => token, + None => { + warn!("No media file token provided"); + return Err(CommonWebError::BadInputWithSimpleMessage("No media file token provided".to_string())); + } + }; + + if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) { + return Err(CommonWebError::BadInputWithSimpleMessage(reason)); + } + + insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool) + .await + .map_err(|err| { + error!("Error inserting idempotency token: {:?}", err); + CommonWebError::BadInputWithSimpleMessage("invalid idempotency token".to_string()) + })?; + const IS_MOD : bool = false; + + let media_file_lookup_result = get_media_file( + media_file_token, + IS_MOD, + &server_state.mysql_pool, + ).await; + + let media_file = match media_file_lookup_result { + Ok(Some(media_file)) => media_file, + Ok(None) => { + warn!("MediaFile not found: {:?}", media_file_token); + return Err(CommonWebError::NotFound); + }, + Err(err) => { + warn!("Error looking up media_file: {:?}", err); + return Err(CommonWebError::ServerError); + } + }; + + if !media_file.media_type.is_jpg_or_png_or_legacy_image() { + return Err(CommonWebError::BadInputWithSimpleMessage("Media file must be a JPG or PNG image".to_string())); + } + + let media_domain = get_media_domain(&http_request); + + let bucket_path = MediaFileBucketPath::from_object_hash( + &media_file.public_bucket_directory_hash, + media_file.maybe_public_bucket_prefix.as_deref(), + media_file.maybe_public_bucket_extension.as_deref()); + + let media_links = MediaLinks::from_media_path_and_env( + media_domain, + server_state.server_environment, + &bucket_path); + + info!("Fal webhook URL: {}", server_state.fal.webhook_url); + + let args = Hunyuan3d21Args { + image_url: media_links.cdn_url, + webhook_url: &server_state.fal.webhook_url, + api_key: &server_state.fal.api_key, + }; + + let fal_result = enqueue_hunyuan_3d_2_1_image_to_3d_webhook(args) + .await + .map_err(|err| { + warn!("Error calling enqueue_hunyuan_3d_2_1_image_to_3d_webhook: {:?}", err); + CommonWebError::ServerError + })?; + + let external_job_id = fal_result.request_id + .ok_or_else(|| { + warn!("Fal request_id is None"); + CommonWebError::ServerError + })?; + + info!("Fal request_id: {}", external_job_id); + + let ip_address = get_request_ip(&http_request); + + let db_result = insert_generic_inference_job_for_fal_queue(InsertGenericInferenceForFalArgs { + uuid_idempotency_token: &request.uuid_idempotency_token, + maybe_external_third_party_id: &external_job_id, + fal_category: FalCategory::ObjectGeneration, + maybe_inference_args: None, + maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token), + maybe_avt_token: maybe_avt_token.as_ref(), + creator_ip_address: &ip_address, + creator_set_visibility: Visibility::Public, + mysql_pool: &server_state.mysql_pool, + }).await; + + let job_token = match db_result { + Ok(token) => token, + Err(err) => { + warn!("Error inserting generic inference job for FAL queue: {:?}", err); + return Err(CommonWebError::ServerError); + } + }; + + Ok(Json(GenerateHunyuan21ImageTo3dResponse { + success: true, + inference_job_token: job_token, + })) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/object/generate_hunyuan2_image_to_3d_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/object/generate_hunyuan_2_image_to_3d_handler.rs similarity index 100% rename from crates/service/web/storyteller_web/src/http_server/endpoints/generate/object/generate_hunyuan2_image_to_3d_handler.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/generate/object/generate_hunyuan_2_image_to_3d_handler.rs diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/object/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/object/mod.rs index 4d56b7015c..fc06b7c299 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/object/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/object/mod.rs @@ -1 +1,2 @@ -pub mod generate_hunyuan2_image_to_3d_handler; \ No newline at end of file +pub mod generate_hunyuan_21_image_to_3d_handler; +pub mod generate_hunyuan_2_image_to_3d_handler; \ No newline at end of file diff --git a/crates/service/web/storyteller_web/src/http_server/routes/add_generate_routes.rs b/crates/service/web/storyteller_web/src/http_server/routes/add_generate_routes.rs index 32095814e7..cb06a16df1 100644 --- a/crates/service/web/storyteller_web/src/http_server/routes/add_generate_routes.rs +++ b/crates/service/web/storyteller_web/src/http_server/routes/add_generate_routes.rs @@ -3,7 +3,8 @@ use crate::http_server::endpoints::generate::image::generate_flux_1_schnell_text 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 actix_http::body::MessageBody; use actix_service::ServiceFactory; @@ -55,6 +56,10 @@ where .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())) + ) ) ) }